├── LICENSE ├── Part 1 - Setup VSCode.md ├── Part 2 - Webpack.md ├── Part 3 - Unit Testing.md ├── Part 4 - Deploying and browser debugging.md ├── Part 5 - Earlybound Types using dataverse-ify.md ├── Part 6 - Calling the WebApi.md ├── Part 7 - Integration Tests with the WebApi.md ├── Part 8 - Calling JavaScript from a Command Bar Button.md ├── Part 9 - Calling Custom APIs from TypeScript.md ├── README.md ├── code ├── .gitignore ├── AddApplicationUserToEnv.ps1 ├── DataverseSolution │ ├── DataverseSolution.csproj │ ├── DataverseSolution.sln │ ├── packages.config │ ├── spkl.json │ └── spkl │ │ ├── deploy-plugins.bat │ │ ├── deploy-webresources.bat │ │ ├── deploy-workflows.bat │ │ ├── download-webresources.bat │ │ ├── earlybound.bat │ │ ├── instrument-plugin-code.bat │ │ ├── pack+import.bat │ │ ├── packonly.bat │ │ └── unpack.bat ├── New-CrmServicePrincipal.ps1 └── clientjs │ ├── .dataverse-gen.json │ ├── .eslintrc.json │ ├── .prettierrc.json │ ├── .vscode │ ├── launch.json │ └── settings.json │ ├── config │ └── test.yaml │ ├── dist │ └── ClientHooks.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── Forms │ │ ├── AccountForm.ts │ │ └── __tests__ │ │ │ └── unit.AccountForm.test.ts │ ├── Mocks │ │ └── MockServiceClient.ts │ ├── Ribbon │ │ ├── AccountRibbon.ts │ │ └── __tests__ │ │ │ ├── integration.AccountRibbon.test.ts │ │ │ └── unit.AccountRibbon.test.ts │ ├── dataverse-gen │ │ ├── actions │ │ │ └── WinOpportunity.ts │ │ ├── entities │ │ │ ├── Account.ts │ │ │ ├── ActivityParty.ts │ │ │ ├── Email.ts │ │ │ ├── Opportunity.ts │ │ │ └── OpportunityClose.ts │ │ ├── enums │ │ │ ├── account_account_accountcategorycode.ts │ │ │ ├── account_account_accountclassificationcode.ts │ │ │ ├── account_account_accountratingcode.ts │ │ │ ├── account_account_address1_addresstypecode.ts │ │ │ ├── account_account_address1_freighttermscode.ts │ │ │ ├── account_account_address1_shippingmethodcode.ts │ │ │ ├── account_account_address2_addresstypecode.ts │ │ │ ├── account_account_address2_freighttermscode.ts │ │ │ ├── account_account_address2_shippingmethodcode.ts │ │ │ ├── account_account_businesstypecode.ts │ │ │ ├── account_account_customersizecode.ts │ │ │ ├── account_account_customertypecode.ts │ │ │ ├── account_account_industrycode.ts │ │ │ ├── account_account_ownershipcode.ts │ │ │ ├── account_account_paymenttermscode.ts │ │ │ ├── account_account_preferredappointmentdaycode.ts │ │ │ ├── account_account_preferredappointmenttimecode.ts │ │ │ ├── account_account_preferredcontactmethodcode.ts │ │ │ ├── account_account_shippingmethodcode.ts │ │ │ ├── account_account_statecode.ts │ │ │ ├── account_account_statuscode.ts │ │ │ ├── account_account_territorycode.ts │ │ │ ├── activityparty_activityparty_instancetypecode.ts │ │ │ ├── activityparty_activityparty_participationtypemask.ts │ │ │ ├── activitypointer_deliveryprioritycode.ts │ │ │ ├── budgetstatus.ts │ │ │ ├── email_email_correlationmethod.ts │ │ │ ├── email_email_notifications.ts │ │ │ ├── email_email_prioritycode.ts │ │ │ ├── email_email_reminderstatus.ts │ │ │ ├── email_email_remindertype.ts │ │ │ ├── email_email_statecode.ts │ │ │ ├── email_email_statuscode.ts │ │ │ ├── initialcommunication.ts │ │ │ ├── msdyn_travelchargetype.ts │ │ │ ├── need.ts │ │ │ ├── opportunity_msdyn_opportunity_msdyn_forecastcategory.ts │ │ │ ├── opportunity_msdyn_opportunity_msdyn_ordertype.ts │ │ │ ├── opportunity_opportunity_opportunityratingcode.ts │ │ │ ├── opportunity_opportunity_prioritycode.ts │ │ │ ├── opportunity_opportunity_salesstagecode.ts │ │ │ ├── opportunity_opportunity_statecode.ts │ │ │ ├── opportunity_opportunity_statuscode.ts │ │ │ ├── opportunity_opportunity_timeline.ts │ │ │ ├── opportunity_salesstage.ts │ │ │ ├── opportunityclose_OpportunityClose_opportunity_statuscode.ts │ │ │ ├── opportunityclose__opportunityclose_instancetypecode.ts │ │ │ ├── opportunityclose__opportunityclose_prioritycode.ts │ │ │ ├── opportunityclose_opportunityclose_opportunity_statecode.ts │ │ │ ├── opportunityclose_opportunityclose_statecode.ts │ │ │ ├── opportunityclose_opportunityclose_statuscode.ts │ │ │ ├── purchaseprocess.ts │ │ │ ├── purchasetimeframe.ts │ │ │ ├── qooi_pricingerrorcode.ts │ │ │ ├── qooi_skippricecalculation.ts │ │ │ └── socialprofile_community.ts │ │ └── metadata.ts │ └── index.ts │ ├── tsconfig.json │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js └── media ├── Part 1 - Setup VSCode ├── 12b220574659a831a19cd4ce27f1721b.png ├── 14cc9ffc59cf01445c260de0d1316cbf.png ├── 6436517ce7e84bbd405c017843009e9a.png ├── 69b04ab5ead6d00fa0b19b30bf7b7de3.png ├── a5877389423c4d374c24316677dd3ad7.png ├── a963fc4c5173f789a6814e8509f4db41.png ├── c85dffc07d60c8a9603622b7e19f0dea.png ├── fa7db755ef5f0c85ac9c39961ded15d9-1621038041728.png └── fb2dab9465e077f5beaefdf4b3979e00.png ├── Part 2 - Webpack ├── 48230f27d21dfff8371dbb8b915e5ab9.png ├── 797a523f367ae1140cded6a2929045a3-1621037998274.png ├── 809d9b6210b8f2d9d1116fc880e0fceb.png ├── 9c467439ede47c447d1c33150aad3dc8.png ├── c6e0e091c1f9bead418037ff47c7c60d.png ├── da2f58d327dbcbd892a6ed9626f87d7b.png └── image-20210514171906706.png ├── Part 3 - Unit Testing ├── 23874f760132e818a03f03b510acf846.png ├── 87d87b9bb8fc36d500891cd02ecc700f.png ├── 8ef4aa8ab2fda103b8834df49da58365-1621039166048-1621039464464.png └── image-20210514173920133.png ├── Part 4 - Deploying and browser debugging ├── 1be8153c41337664be9fcf5f9fe88f53-1621041352659.png ├── 209f8c510960f8376781f1dc98624a25-1621041294187.png ├── 2d9691975a3925b38d5183bcf4bdc143-1621041304252.png ├── 8df3f8e76e8e6caaac7e72ca34f51802-1621040814229-1621041273678.png ├── a8d3ba6ad377f718710bfe32264ecb2b-1621040812317-1621041288383.png ├── ab3a009fb2a013645d6c913ac8b2ca96-1621041308204-1621041350541.png ├── c90950766b208d54393f75095d4eee82-1621041314360.png ├── d156b381057756735ead6da6c506b00d-1621041291312.png ├── d609aacde1ede48cda732dd35e8ba0b8.png ├── f6b364cb67673f9f9347ad50d601a6df-1621041312686.png └── faa5aeb1be596978bf2ddfc62bfb7715-1621041310845.png ├── Part 5 - Earlybound Types and the WebApi ├── 02a54b090272a722073f769cc3e96466.png └── a577fec6dc0cef026a376e47808783fb.png ├── Part 5 - Earlybound Types using dataverse-ify ├── d00ae9a1bdcd9741a805cb7f4cdb3cc1.png └── df10f5ab8582f9ffcb51b388e7fdda7e.png ├── Part 7 - Calling JavaScript from a Command Bar Button ├── 1dd3f66d3ab0cd6cec3cefbc49c5275b.png ├── 848470f7cc006b927e03460a3a153061.png ├── 85bc10820f16cbdb4c1a6c18d4092a98.png └── image-20210516124244333.png └── Part 9 - Calling Custom APIs from TypeScript ├── image-20210524113658317.png ├── image-20210524114240721.png ├── image-20210524114617775.png ├── image-20210524114936748.png ├── image-20210524115041856.png ├── image-20210524120210899.png ├── image-20210524120919154.png ├── image-20210524135145168.png ├── image-20210524135303186.png ├── image-20210524140229468.png ├── image-20210524141338633.png ├── image-20210524142141829.png └── image-20210524143025405.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Scott Durow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Part 1 - Setup VSCode.md: -------------------------------------------------------------------------------- 1 | # Part 1 - Setting up VSCode for Developing JavaScript Web Resources using TypeScript 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this first part, we will cover setting up your VSCode environment to create TypeScript [Dataverse JavaScript Web resources.](https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/web-resources) 6 | 7 | ## Pre-Requisites 8 | 9 | Before you start, you will need to install the following: 10 | 11 | - **NodeJS** - 12 | - **VSCode** - 13 | - **Visual Studio** - (Community Edition is free!) 14 | 15 | When installing Node, select the LTS version and accept the defaults. 16 | 17 | 18 | To check you have Node installed correctly, at the command line type: 19 | 20 | ```shell 21 | npm 22 | ``` 23 | 24 | You should see the npm command help information displayed. 25 | 26 | To check you had VSCode installed correctly, at the command line type: 27 | 28 | ```shell 29 | Code . 30 | ``` 31 | 32 | This should open VSCode at the folder location you are currently in. 33 | 34 | ## TypeScript VSCode Quick Start 35 | 36 | ### Project folder setup 37 | 38 | VSCode TypeScript projects do not have a project file like C\# (`.csproj`) – so 39 | you can simply create a new folder with the name of your project. I will use 40 | `clientjs.` 41 | 42 | At the command line, type: 43 | 44 | ```shell 45 | mkdir clientjs 46 | cd clientjs 47 | ``` 48 | 49 | ### Npm setup 50 | 51 | Npm is used to install required modules into a node_modules folder. To intialise 52 | your project type: 53 | 54 | ```shell 55 | npm init 56 | ``` 57 | 58 | Press Return on each prompt to accept the defaults. 59 | 60 | ### TypeScript setup 61 | 62 | TypeScript is used to initialise the project folder with a `tsconfig.json` file. 63 | 64 | At the command line, type: 65 | 66 | ```shell 67 | npm install typescript --save-dev 68 | npx tsc -init 69 | Code . 70 | ``` 71 | 72 | You should see VSCode open with a project similar to: 73 | 74 | **** 75 | 76 | Open the tsconfig.json file and make the following changes: 77 | 78 | ```json 79 | "module": "es2015", 80 | "lib": ["es2015","dom"], 81 | "rootDir": "src", 82 | "moduleResolution": "node", 83 | "sourceMap": true, 84 | ``` 85 | 86 | - **`module` -** This is important to set to es2015 so that webpack can 87 | ‘tree-shake’ and decide which modules it needs to output in the bundle. 88 | 89 | - **`lib`** tells typescript that we can use the ES2015 features (e.g. Promise) 90 | and HTML Dom libraries because they will be available at runtime. 91 | 92 | - **`rootDir`** – We are putting our TypeScript inside the src folder. This is a 93 | common convention and separates the TypeScript from other resources that we 94 | may have in our project. 95 | 96 | - **`moduleResolution` –** This tells TypeScript that we are writing our code in 97 | the same way that we would load modules when running inside a Node 98 | environment. This is because later, webpack will work out how to package the 99 | modules that we are using so that they will run inside the browser. 100 | 101 | - **`sourceMap` –** This tells TypeScript that we want to produce sourcemaps for 102 | our TypeScript code so that webpack can package them when creating 103 | development builds for debugging inside the browser. 104 | 105 | > **Important**: The node_modules contains the files that are downloaded by 106 | > npm. They are not necessary to be checked into source-code and can be 107 | > re-installed at anytime by deleting the node_modules folder and running the 108 | > command: `npm install` 109 | 110 | ### Install `ESLint` & `prettier` 111 | 112 | You should always use a linter with your TypeScript projects to catch common 113 | issues and promote best practices. ESLint is the most common linter used with 114 | TypeScript today. prettier then ensures your code is always formatted 115 | consistently so that you do not get noisy diffs when committing to source 116 | control. 117 | 118 | Grab the following files and copy them to your project folder: 119 | 120 | - [.eslintrc.json](https://raw.githubusercontent.com/scottdurow/dataverse-jswebresource-template/master/.eslintrc.json) 121 | 122 | - [.prettierrc.json](https://raw.githubusercontent.com/scottdurow/dataverse-jswebresource-template/master/.prettierrc.json) 123 | 124 | > **NOTE:** the leading full stop (.) on the filename is very important! 125 | 126 | At the command line type: 127 | 128 | ```shell 129 | npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react eslint-config-prettier eslint-plugin-prettier 130 | 131 | npm install --save-dev --save-exact prettier 132 | ``` 133 | 134 | This will install the eslint and prettier node modules. 135 | 136 | Now you have eslint configured, you can add linting to your `package.json` file to enable you to list and fix any code issues. Add the following scripts: 137 | 138 | ```json 139 | "scripts": { 140 | 141 | "lint": "eslint src --ext .ts", 142 | "lint:fix": "npm run lint -- --fix" 143 | 144 | } 145 | ``` 146 | 147 | At the command line you should now be able to run: 148 | 149 | ```shell 150 | npm run lint 151 | ``` 152 | 153 | and to fix any issues that are reported that can be fix automatically use: 154 | 155 | ```shell 156 | npm run lint:fix 157 | ``` 158 | 159 | See https://eslint.org/docs/user-guide/command-line-interface for more information. 160 | 161 | ### Install the `ESLint` VSCode Extension 162 | 163 | We will uses the ESLint VSCode extension. This will give you a code lens that 164 | provides feedback of any linting/prettifier issues. 165 | 166 | 1. Install the ESLint Marketplace extension to VSCode. The ESLint extensions 167 | simply uses the ESLint configuration of your project. 168 | 169 | 2. Add a new file under a folder named `src`, named `index.ts` - this will mean that ESLint will start running on our project. 170 | 3. Close and Re-open VSCode ensure that ESLint is running. You should now see 171 | ESLint in the status bar of VSCode: 172 | 173 | *Note: The ESLint extension will pick up the eslint configuration files you 174 | have – it will not run on projects that are not configured to work with 175 | eslint.* 176 | 4. Open Settings in VSCode, and type ‘code action on save’ 177 | 5. Under the **Workspace Tab**, select **Edit in settings.json** under **Editor: Code Actions On Save’** 178 | 179 | 6. In the `settings.json`, ensure it is set to: 180 | 181 | ```json 182 | { 183 | "editor.codeActionsOnSave": { 184 | "source.fixAll": true 185 | } 186 | } 187 | ``` 188 | 189 | This will now ensure your code files are formatted consistently when saving. 190 | 191 | > **Note:** There is one downside with this setting in that it will often 192 | > result in longer save-times. You may prefer to use the command `ESLint: Fix 193 | > all auto fixable problems` instead. You can assign a keyboard short-cut to 194 | > this command – I use `Shift + Alt + P 195 | > `![](media/Part 1 - Setup VSCode/a963fc4c5173f789a6814e8509f4db41.png) 196 | 197 | ### Install `@types/xrm` 198 | 199 | The JavaScript we are going to write will use the Xrm Client Api. To enable 200 | strong types in our code we install the `@types/xrm` node module. 201 | 202 | 1. At the command line, Type: 203 | 204 | ```shell 205 | npm install --save-dev @types/xrm 206 | ``` 207 | 208 | 2. Add a new folders and file `src/Forms/AccountForm.ts` 209 | 210 | 3. Add the following code: 211 | 212 | ```typescript 213 | export class AccountForm { 214 | static async onload(context: Xrm.Events.EventContext): Promise { 215 | context 216 | .getFormContext() 217 | .getAttribute("name") 218 | .addOnChange(() => { 219 | console.log("name onchange"); 220 | }); 221 | } 222 | } 223 | ``` 224 | 225 | If you type this in manually, you will see the intellisense for the Xrm types 226 | that is provided by the module `@types/xrm`. 227 | 228 | 229 | If you make any changes to this file you may start to see red underlined areas 230 | that indicate ESLint formatting issues: 231 | 232 | 233 | When you save, these should be auto-fixed if you have that setting turned on. If 234 | not, use the VSCode command `ESLint: Fix all auto-fixable Problems` 235 | 236 | You may also see yellow underlined areas where there are ESLint issues: 237 | 238 | 239 | These cannot be auto-fixed, but you may ignore if needed by pressing `Ctrl + .` 240 | or ‘Quick Fix..’ and then **Disable for this line** or **Disable for the entire 241 | file**. This will add a comment into your code telling ESLint that you are ok to 242 | ignore the issues. 243 | 244 | ## Up Next... 245 | 246 | In the next part we will look at using `webpack` to bundle our transpiled JavaScript so that it can be run inside the browser. 247 | 248 | -------------------------------------------------------------------------------- /Part 2 - Webpack.md: -------------------------------------------------------------------------------- 1 | # Part 2 – Setting up Webpack to create a JavaScript Web resource bundle 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this second part, we will cover how to create your Web Resource JavaScript from your TypeScript project. 6 | 7 | When writing TypeScript, usually the following is true: 8 | 9 | 1. You span your TypeScript across multiple source files that need to be compiled into a single JavaScript file 10 | 11 | 2. You import modules from `node_modules` that need to be also compiled into the single JavaScript file 12 | 13 | 3. Your JavaScript typically will include source maps to enable debugging during development, but should be minified (optimised in size) for production distribution. 14 | 15 | Webpack is a tool that addresses each of these issues by taking the output from the TypeScript compiler (`tsc`), parses the files and bundles them along with any required node modules into a single JavaScript file that can be run inside the browser. 16 | image-20210514171906706 17 | 18 | ## Babel vs TypeScript JavaScript Transpilation 19 | 20 | `Webpack` can be used in conjunction with another library called `Babel`. In a similar way to the TypeScript compiler (`tsc`), Babel can take your TypeScript code and translate it into JavaScript (often referred to as transpiling) so that it will run inside the browser. You might use Babel if you needed more control over the final output such as adding polyfills, using features that the TypeScript compiler doesn’t support or provide multiple versions targeting different run-times. I find no need to use Babel for creating JavaScript Webresources. If you are using Babel then make sure you are using a loader that runs the TypeScript compiler to perform type checking since Babel on it’s own will simple convert to JavaScript without performing any checks. 21 | 22 | Using the `ts-loader` webpack plugin will run the tsc compiler for you so that all your types are checked – but if you were using an alternative Babel loader, you can always run `tsc --noEmit` to run the TypeScript compiler and show errors, but not generate any output. 23 | 24 | ## Install & Configure Webpack 25 | 26 | Run the following at the command line of your VSCode Project: 27 | 28 | ```shell 29 | npm install webpack webpack-cli webpack-merge ts-loader --save-dev 30 | ``` 31 | 32 | Download and copy the following Webpack configuration files to your project folder: 33 | 34 | [webpack.common.js](https://raw.githubusercontent.com/scottdurow/dataverse-jswebresource-template/master/webpack.common.js) 35 | 36 | [webpack.dev.js](https://raw.githubusercontent.com/scottdurow/dataverse-jswebresource-template/master/webpack.dev.js) 37 | 38 | [webpack.prod.js](https://raw.githubusercontent.com/scottdurow/dataverse-jswebresource-template/master/webpack.prod.js) 39 | 40 | The core webpack configuration is found in `webpack.common.js` This file is automatically used by webpack. For Dev and Prod build, the common configuration is merged with the specific settings (using `webpack-merge`) for the target environment. 41 | 42 | Open the `webpack.common.js` and edit the library settings, to define the filename and your JavaScript webresource namespace. 43 | 44 | ```json 45 | filename: "bundle-name.js", 46 | library: ["namespacepart1","namespacepart2"] 47 | ``` 48 | In our project this looks like this: 49 | 50 | 51 | If you simply wanted to namespace your code under foo, you could use: 52 | 53 | ```json 54 | library: ["foo"], 55 | ``` 56 | 57 | This would expose your code under the library name `foo` 58 | 59 | ### Configuring webpack to run using `package.json` scripts 60 | 61 | Open the `package.json` file and add the following scripts, overwriting the existing scripts section created by `npm --init` 62 | 63 | ```json 64 | "scripts": { 65 | "build": "webpack --config webpack.dev.js", 66 | "start": "webpack --config webpack.dev.js --watch", 67 | "dist": "webpack --config webpack.prod.js" 68 | }, 69 | ``` 70 | 71 | This allows us to run the development build at the command line using: 72 | 73 | ```shell 74 | npm start 75 | ``` 76 | 77 | If wanted to create a distribution production build, we would use: 78 | 79 | ```shell 80 | npm run-script dist 81 | ``` 82 | 83 | Later in this series, we will create unit-test and integration-test scripts and add them to this section of the `package.json`. 84 | 85 | ## Create an entry point. 86 | 87 | A key feature of webpack is that it will determines which modules are required to be bundled into the output JavaScript by looking at your code and the modules that it imports. To determine what is needed to be bundled, an entry point must be provided. The default is `index.ts`. 88 | 89 | > **Note:** Since TypeScript 1.8, the tsc compiler has had the `-outFile` that creates a single bundled file similar to Webpack, however webpack gives much finer control over how this bundle is generated. 90 | 91 | Create a file in the src folder named `index.ts` and add the following TypeScript: 92 | 93 | ```typescript 94 | export * from "./Forms/AccountForm"; 95 | ``` 96 | 97 | Add the command line, type: 98 | 99 | ```shell 100 | npm start 101 | ``` 102 | 103 | This will start webpack in watch mode, so that as you change your code the bundle will incrementally be compiled. You will find the output JavaScript in `dist/ClientHooks.js` 104 | 105 | > **Note:** You can change the name of entry point and the output file in the `webpack.common.js` file. 106 | 107 | If you look at the `ClientHooks.js` you will see that it looks very different to your TypeScript files. This is because it is transpiled to `ES5` JavaScript and includes source maps. Source maps provide the information that is needed to debug the TypeScript code even though it is actually JavaScript being executed. If you are used to debugging c\# you can think of the source-maps in a similar way to the `.pdb` files created in Debug configuration builds of c\# code. 108 | 109 | 110 | > **Note:** The Underlined red code is because this code does not conform to ESLint’s formatting rules. 111 | 112 | If you search for `sourceMappingURL=` you will find your original TypeScript code base64 encoded. Later we will see how you can step through your TypeScript code in the browser via the webpack: source files even though it is actually JavaScript running. 113 | 114 | > Note: The original TypeScript output is also visible as source files under the **webpack-internal:** prefix – but normally you do not need to view these source maps – it will be the source files prefixed with **webpack:** that we are interested in. 115 | 116 | 117 | 118 | 119 | If you make a change to the `AccountForm.ts` you will see that it is detected, and the `dist/ClientHooks.js` file is updated: 120 | 121 | 122 | If you added additional modules that must be exported that are not themselves referenced by the `AccountForm.ts`, then they must be included in the `index.ts`. If however you simply import them inside the `AccountForm.ts`, they will be automatically detected and bundled. We will see this in action later in this series. 123 | 124 | If you press `Ctrl + C` to stop the watch process, you can then create a production build by typing 125 | 126 | ```shell 127 | npm run-script dist 128 | ``` 129 | 130 | This will create a minified version of your JavaScript: 131 | 132 | 133 | Later we will see how to deploy these JavaScript files into a Model Driven App. 134 | 135 | We can see the Module bundling take place by adding the `moment` library so that we can manipulate and parse dates. At the command line, type: 136 | 137 | ```shell 138 | npm install moment 139 | ``` 140 | 141 | You will see in the `packages.json` a new entry that defines the required module. This will be installed later if needed during a build by using `npm install`. 142 | 143 | 144 | You can now use this module in the `AccountForm.ts`: 145 | 146 | ```typescript 147 | import moment from "moment"; 148 | export class AccountForm { 149 | static async onload(context: Xrm.Events.EventContext): Promise { 150 | const now = moment().format(); 151 | context 152 | .getFormContext() 153 | .getAttribute("name") 154 | .addOnChange(() => { 155 | console.log(`name onchange ${now}`); 156 | }); 157 | } 158 | } 159 | ``` 160 | 161 | If you run `npm start`, you will now see the `dist/ClientHooks.js` has the moment library bundled into it. A key principle to bundling is that internal modules are not exposed in the global namespace. This allows for multiple versions to be loaded side-by-side at runtime if needed by different components. 162 | 163 | > **Note:** It used to be a common practice to load additional modules on demand rather than bundling - this practice is generally accepted as not best practice due to the additional http requests required and deployment complexity. 164 | 165 | ## ES5 vs ES6+ 166 | 167 | TypeScript allows us to write code using some of the rich language features of the more recent JavaScript standards. When the `tsc` compiler is used to combine into JavaScript, you can target which version of JavaScript you want to support using the target configuration of the `tsconfig.json` file. Currently if you need to support IE11 (which is still used by some version of the outlook App), then the `tscofing.json` must specify targeting `es5`. 168 | 169 | ```json 170 | { 171 | "compilerOptions": { 172 | "target": "es5", 173 | ... 174 | ``` 175 | 176 | One example of this is the use of the `async` /`await` pattern. This allows us to easily call asynchronous code that returns a `Promise` without the need for call-backs. If the `tsconfig.ts` defines the output to be es5, since `await` is not part of the es5 standard it is compiled into JavaScript using the older call-back style code. 177 | 178 | As browsers support more modern features, the target can be updated to output JavaScript according to a later standard, and consequently will be much closer to the original TypeScript. See the following table for the features supported by browsers that are part of es6 179 | 180 | You can change the target to es6 if you do not need to support IE11. This will reduce the complexity of the transpiled JavaScript since it can use language features such as classes. 181 | ```json 182 | { 183 | "compilerOptions": { 184 | "target": "es6", 185 | ... 186 | ``` 187 | 188 | 189 | ## Tree shaking 190 | 191 | One advantage of using `Webpack` is something called [tree shaking](https://webpack.js.org/guides/tree-shaking) . If you are importing a module that has many different exports but you only use some of them, Webpack will work out that it only needs to bundle the code that you use. This can reduce the size of you bundled JavaScript, but the extent to which it makes a difference depends on the module structure of the modules you are consuming. See 192 | 193 | To ensure that tree shaking can be used on your code, the `tsconfig.json` must include: 194 | 195 | ```json 196 | "module": "es2015" 197 | ``` 198 | 199 | If you use `commonjs` (the default), then any import/exports will be converted to code that webpack will not be use to tree shake. 200 | **IMPORTANT:** You will only see tree shaking happen when you do a production build of your code - the development build will not remove un-used exports. 201 | 202 | > **Note:** Tree shaking will only have a significant effect on your bundle size if you are using very large libraries that have many components that you are not using, or if your TypeScript code contains large amounts of redundant exports that are not used. 203 | 204 | ## Up Next... 205 | 206 | In the next part we will look at adding unit-tests using a library called `jest`. 207 | 208 | -------------------------------------------------------------------------------- /Part 3 - Unit Testing.md: -------------------------------------------------------------------------------- 1 | # Part 3 – Developing and Testing locally inside VSCode 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this third part we will cover developing your TypeScript web resource and testing first before deploying to your Dataverse environment. 6 | 7 | ## Install `jest` and `xrm-mock` 8 | 9 | Our unit tests will use a library called `jest` to define tests and `xrm-mock` to simulate the `Xrm` Client Side API. 10 | 11 | At the command prompt of your VSCode project, type: 12 | 13 | ```shell 14 | npm install jest ts-jest xrm-mock @types/jest --save-dev 15 | ``` 16 | 17 | - `jest` – a library that is used to write and run tests (https://jestjs.io/) 18 | - `ts-jest` – a jest pre-set that allows using `jest` with TypeScript 19 | - `xrm-mock` – a library for mocking the form context in Model Driven Apps () 20 | 21 | ### Add `jest.config.js` 22 | 23 | Add the `jest.config.js` to the root of your project: 24 | 25 | ```json 26 | module.exports = { 27 | preset: "ts-jest", 28 | testEnvironment: "node", 29 | }; 30 | ``` 31 | 32 | This file is automatically used by jest when running tests and used to determine the settings to use. 33 | 34 | - `ts-jest` - this tells jest that our tests are written in TypeScript and must be transpiled first 35 | - `node` - this is the environment that our tests will run in. Another valid option is us `jsdom` that would emulate running inside a browser (by adding features such as `window`, `document` and `DomParser`) - however our tests will use node so that they can communicate with the Dataverse environment later when we write integration tests. 36 | 37 | ### Add `launch.json` 38 | 39 | When you press F5 inside a unit test, it is convenient to have VSCode allow you to debug your unit tests and hit breakpoints. To setup this up, add a `launch.json` to your `.vscode` folder: 40 | 41 | 42 | 43 | > Note: If you don’t add a `jest.config` then you will get the error ‘SyntaxError: Cannot use import statement outside a module’ later when you run the tests. 44 | 45 | ## Create unit tests for Account Form onload 46 | 47 | Create a new folder for our Account Form logic tests: `src/Forms/__tests__` 48 | 49 | > **Note:** The `__tests__` folder is the default name used by jest to search for tests, but this can be changed inside the `jest.config `(see https://jestjs.io/docs/configuration). 50 | 51 | Create a new file for our TypeScript tests: `src/Forms/__tests__/unit.AccountForm.test.ts` 52 | 53 | > **Note:** by naming the file `.test.ts` – VSCode will show the icon of the file differently to normal TypeScript files. By prefixing the file with unit we can later select which type of tests to run easily (e.g. unit or integration tests). 54 | 55 | ### Add a simple test 56 | 57 | The jest library provides a very simple way of defining tests: 58 | 59 | - `describe` is used to divide your tests into components – you can nest describe if needed. 60 | - `it` is used to describe an individual sentence – add the description so that it reads like a sentence. 61 | - `beforeEach` - a function that is run before each `it` test. This is useful for adding mocking code that is used by every test in the describe suite. Tests in each `describe` suite are run in sequence that they appear. 62 | 63 | Our Account Form onload function will add an event that validates the format of URLs entered into the website field. Add the following to `src\Forms\__tests__\unit.AccountForm.test.ts`: 64 | 65 | ```typescript 66 | import { AccountForm } from "../AccountForm"; 67 | import { XrmMockGenerator } from "xrm-mock"; 68 | 69 | describe("AccountForm.onload", () => { 70 | beforeEach(() => { 71 | XrmMockGenerator.initialise(); 72 | }); 73 | 74 | it("notifies invalid website addresses", () => { 75 | const context = XrmMockGenerator.getEventContext(); 76 | const websiteMock = XrmMockGenerator.Attribute.createString("websiteurl", "foobar"); 77 | websiteMock.controls.itemCollection[0].setNotification = jest.fn(); 78 | AccountForm.onload(context); 79 | websiteMock.fireOnChange(); 80 | expect(websiteMock.controls.itemCollection[0].setNotification).toBeCalled(); 81 | }); 82 | 83 | it("clears notification on valid website address", () => { 84 | const context = XrmMockGenerator.getEventContext(); 85 | const websiteMock = XrmMockGenerator.Attribute.createString("websiteurl", "foo"); 86 | websiteMock.controls.itemCollection[0].setNotification = jest.fn(); 87 | websiteMock.controls.itemCollection[0].clearNotification = jest.fn(); 88 | AccountForm.onload(context); 89 | websiteMock.fireOnChange(); 90 | expect(websiteMock.controls.itemCollection[0].setNotification).toBeCalledWith(expect.any(String), "websiteurl"); 91 | 92 | websiteMock.value = "https://learn.develop1.net"; 93 | websiteMock.fireOnChange(); 94 | expect(websiteMock.controls.itemCollection[0].clearNotification).toBeCalledWith("websiteurl"); 95 | }); 96 | }); 97 | 98 | ``` 99 | 100 | If you now run jest, you will see that these two test fail since we have not implemented the logic yet! 101 | 102 | At the command prompt of your VSCode project, type: 103 | 104 | ```shell 105 | npx jest 106 | ``` 107 | 108 | Or you can install `jest` globally using: 109 | 110 | ```shell 111 | npm install jest -g 112 | ``` 113 | 114 | This would allow you to then simply use the following at the command line: 115 | 116 | ```shell 117 | jest 118 | ``` 119 | 120 | 121 | 122 | These tests obviously will fail at the moment because we have not added any code! 123 | 124 | To define which unit tests we should run for the project, we can add the following script to the `package.json`: 125 | 126 | ```json 127 | "scripts": { 128 | 129 | "test": "jest", 130 | 131 | ``` 132 | 133 | This allows us to then simply run the following to run the tests: 134 | 135 | ```shell 136 | npm test 137 | ``` 138 | 139 | Later, we will define a different set of tests for integration and unit tests using different scripts to run the different suites of tests. 140 | 141 | ### Write the code 142 | 143 | Currently the code does not perform any validation, so we add the following to the `src\Forms\AccountForm.ts` 144 | 145 | ```typescript 146 | export class AccountForm { 147 | static async onload(context: Xrm.Events.EventContext): Promise { 148 | context.getFormContext().getAttribute("websiteurl").addOnChange(AccountForm.onWebsiteChanged); 149 | } 150 | static onWebsiteChanged(context: Xrm.Events.EventContext): void { 151 | const formContext = context.getFormContext(); 152 | const websiteAttribute = formContext.getAttribute("websiteurl"); 153 | const websiteRegex = /^(https?:\/\/)?([\w\d]+\.)?[\w\d]+\.\w+\/?.+$/g; 154 | 155 | let isValid = true; 156 | if (websiteAttribute && websiteAttribute.getValue()) { 157 | const match = websiteAttribute.getValue().match(websiteRegex); 158 | isValid = match != null; 159 | } 160 | 161 | websiteAttribute.controls.forEach((c) => { 162 | if (isValid) { 163 | (c as Xrm.Controls.StringControl).clearNotification("websiteurl"); 164 | } else { 165 | (c as Xrm.Controls.StringControl).setNotification("Invalid Website Address", "websiteurl"); 166 | } 167 | }); 168 | } 169 | } 170 | ``` 171 | 172 | This code uses the `@types/xrm` definitions we imported earlier to easily access the Client Side SDK in a strongly typed way. This is one of the significant advantages of using TypeScript since the types will be checked when transpiling. You will also note `ESLint` doing it's job and ensuring your code is consistent in layout and style. 173 | 174 | See more info on `setNotification` here -https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/controls/setnotification 175 | 176 | ## Debug your tests 177 | 178 | If all of the above has been performed correctly, you now should be able to open your `unit.AccountForm.test.ts` file in VSCode and add a breakpoint, then press F5 to start debugging. 179 | 180 | VSCode should then allow you to debug your tests and step through each line using the standard debug controls. 181 | image-20210514173920133 182 | 183 | If you type `jest` at the command line you will also see that now your tests pass: 184 | 185 | 186 | If you wanted to specifically test an individual test you can also use the following (after jest is installed globally as described above): 187 | 188 | ```shell 189 | jest unit.AccountForm 190 | ``` 191 | 192 | This will just run the single `AccountForm` test suite by using the parameter provided to match against the TypeScript file names that contain tests. 193 | 194 | ## A note about VSCode Test Runners 195 | 196 | There are many excellent VSCode extensions that provide a convenient user interface and code lens for your tests. I would recommend starting with the command line approach first, and then move to a user interface provided by an extension. The extensions are simply automating what you would be doing at the command line. 197 | 198 | ## Up Next... 199 | 200 | Now that you have built, tested and bundled your JavaScript Webresource, the next step is to deploy it and test it inside the browser. 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /Part 4 - Deploying and browser debugging.md: -------------------------------------------------------------------------------- 1 | # Part 4 – Deploying Webresources using spkl and debugging inside the browser 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | Now that you have created and tested your webresource locally, you are ready to deploy it to your Dataverse environment and hook it up to the Model Driven App. 6 | 7 | ## Deploying using spkl 8 | 9 | `spkl` is a command line utility () that is distributed via NuGet. It allows you to perform common Dataverse tasks such as deploying Plugins and Webresources via a set of simple command files. 10 | 11 | #### Create Visual Studio Project 12 | 13 | The `spkl` NuGet package can easily be downloaded and run using Visual Studio’s NuGet integration. Start by using Visual Studio to create a new C\# Class Library Project (it does not really matter what type as long as it targets the .Net Framework and not .Net Core). 14 | 15 | > **Note**: You can use Visual Studio Community Edition ( ) if you don’t have Visual Studio already installed. 16 | 17 | 18 | 19 | 20 | Select the location of your project to be in the same folder as the VSCode project that you have created for your TypeScript. 21 | 22 | In the newly created project: 23 | 24 | 1. Right-Click on the **Project** -\> **Manage NuGet Packages** 25 | 26 | 2. Select the **Browse** tab and type **spkl**. If you want to use the more recent beta features, select **Include prerelease**: 27 | 28 | 29 | 3. Select **spkl** and then **Install** 30 | 31 | 4. You will now see a **spkl** folder and **`spkl.json`** in your project. You can delete the other two c\# files since they are not needed: 32 | 33 | 34 | 35 | #### Edit the `spkl.json` 36 | 37 | You next need to tell `spkl` where your built JavaScript files are to deploy. 38 | 39 | Open the `spkl.json` and replace it with the following: 40 | 41 | ```json 42 | { 43 | "webresources": [ 44 | { 45 | 46 | "profile": "default,debug", 47 | "solution": "SOLUTION_UNIQUE_NAME", 48 | "files": [ 49 | { 50 | "uniquename": "dev1_/js/clienthooks.js", 51 | "file": "../clientjs/dist/clienthooks.js", 52 | "description": "JavaScript for Forms and Commandbar Actions" 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | The webresources section tells `spkl` where the files are you want to deploy. 61 | 62 | Replace `SOLUTION_UNIQUE_NAME` with your specific solution name. The deployed webresource will be added to this solution. 63 | 64 | > **Note**: Later in this series we will add another section under solutions to control how the solution will be unpacked and the JavaScript picked up from the build output. 65 | 66 | ### Deploying the webresources 67 | 68 | You can now easily deploy your webresource using the batch file found under `spkl`. 69 | 70 | If you are working in VSCode, you don’t need to have Visual Studio open, you can simply use the terminal to change the directory using the relative path to your spkl folder: 71 | 72 | ```shell 73 | cd ..\DataverseSolution\spkl 74 | deploy-webresources.bat 75 | ``` 76 | 77 | > **Note:** It is important that you run the batch files when in the folder that contains the `spkl.json` or a child directory of that location. `spkl` will search for the nearest `spkl.json` to use. 78 | 79 | 80 | 81 | The webresource is deployed or an existing one updated. It is added to the solution if not already there and then the changes are published. 82 | 83 | You can then hook up your webresource to the necessary form events. In this example we register it on the Account form for the Onload event: 84 | 85 | **Event Type**: On Load 86 | 87 | **Library**: `dev1_/js/clientooks.js` 88 | 89 | **Function**: `cds.ClientHooks.AccountForm.onload` 90 | 91 | **Pass execution context:** Checked 92 | 93 | 94 | 95 | 96 | ## Debugging in the browser 97 | 98 | When you have webpack running in watch mode (`npm start`) the output JavaScript will update each time you make a change. Instead of constantly deploying your webresource with each change you can easily use a tool called Fiddler to redirect requests to your webresource on the server to the local file that is being updated by webpack. See 99 | 100 | #### Download and install Fiddler Classic 101 | 102 | There is a new version of Fiddler called ‘Fiddler Everywhere’ – but instead you should download ‘Fiddler Classic’ 103 | 104 | Download and install from: 105 | 106 | After installation, you must enable HTTPS decryption so that we can intercept the traffic between the browser and the Dataverse environment. 107 | 108 | 1. Open **Tools** -\> **Options** -\> **HTTPS** Tab 109 | 110 | 2. Check the **Decrypt HTTPS traffic** and accept the prompts to install the Fiddler Certificate. 111 | 112 | 113 | 3. You can now select the **AutoResponder** tab, and select **Add Rule** 114 | 115 | 4. Set the Match string to be the webresource name and the Path to the full path of your webpack generated output: 116 | 117 | - `dev1_/js/clienthooks.js` - Enter the unique name of your webresource. 118 | - Enter the full path of your `dist/ClientHooks.js` 119 | 120 | 121 | ab3a009fb2a013645d6c913ac8b2ca96-1621041308204-1621041350541 122 | 123 | **Note:** This can also be a regex if you had multiple webresources at the same location: 124 | 125 | ```text 126 | StringToMatch: REGEX:(?insx).+\/dev1_\/js\/(?'fname'[^?]*.js)` 127 | File: `C:\MyProject\dist\${fname} 128 | ``` 129 | 130 | Be sure to select **Enable rules** and **Unmatched requests passthrough**. 131 | 132 | 133 | Now when you refresh your browser, the webpack generated file will be loaded into the browser. 134 | 135 | 5. Navigate your Dataverse App in the browser and press F12 (or `Ctrl + Shift + I`) to open the Developer Tools. 136 | 137 | 6. In The Developer Tools open the settings and check **Disable cache (while DevTools is open)**. This will ensure that the latest version is always requested when you refresh a page. 138 | 139 | 140 | 141 | 142 | 7. Ensure you have webpack running in watch mode (`npm start`) 143 | 144 | 8. Navigate to an Account Record in your Model Driven App to load the Webresource. 145 | 146 | 9. In the Developer Tools use Open File (`Ctrl + P`) and type the name of a TypeScript file, you should see two versions of it – one prefixed with **`webpack-internal://`** and the other just with **`webpack://`** 147 | 148 | 149 | 10. Be sure to select the file with the `webpack:` prefix. This is the original TypeScript source that is output under the **`sourceMappingURL`** because your **`webpack.dev.js`** has the setting **`devtool: "eval-source-map"`**. 150 | The `webpack-internal` source is simply the JavaScript that was created by the TypeScript compiled will look somewhat different to your original TypeScript depending on which TypeScript language features you are using and the target you are using. Using ES5 is the most common if you want to IE11 (still used by the App for Outlook when running inside some versions of Office at the time of writing) 151 | 152 | 11. You can now set a break point on the **`AccountForm.ts`**, refresh the Model Driven record and you can then step through your code. 153 | 154 | 155 | 12. If you make a change to your TypeScript, webpack will pick up the modification and regenerate the JavaScript. Refreshing the Model Driven App page will then pick up the latest version (provided Fiddler is running and you have the Developer Tools open). 156 | 157 | 13. When you are stepping through code, you may find that it will step into the underlying webpack-internal code. This is because the TypeScript transpiled files are output using the `eval-source-map` plugin and labelled with `sourceURL=webpack-internal`. 158 | 159 | Once you are happy with the JavaScript webresource, you can then build for distribution and deploy the final version. Later in this series we will look at how to build and deploy your JavaScript webresource as part of a build/release pipeline. 160 | 161 | ```shell 162 | npm run dist 163 | ``` 164 | 165 | Change directory to the spkl folder at the command prompt: 166 | 167 | ```shell 168 | deploy-webresource.bat 169 | ``` 170 | 171 | This will deploy your built web resource without the source-maps and with the code minified for production deployment. 172 | 173 | ## Troubleshooting 174 | 175 | - **TypeScript file not found** - If you don't see your `AccountForm.ts` inside the browser, you may need to perform a Hard Refresh `Ctrl-Shift-R` 176 | - Breakpoint not hit - You may be looking at a stale version of your `AcountForm.ts` - try searching again and ensure that it is prefixed with `webpack` - and not `wepack-internal`. 177 | 178 | ## Up Next... 179 | 180 | Now we are ready to look at some more advanced topics such as early bound types and calling the `WebApi`. 181 | 182 | -------------------------------------------------------------------------------- /Part 5 - Earlybound Types using dataverse-ify.md: -------------------------------------------------------------------------------- 1 | # Part 5 - Early bound types using Dataverse-ify 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this fifth part we will cover creating a early bound types so that you can call the reference attribute names in form scripts and call the `WebApi` with ease. 6 | 7 | `Dataverse-ify` is an open source library that aims to provide an interface to the Dataverse `WebApi` from TypeScript that works in a similar way to the C\# `IOrganisationService` so that you do not need to code around the complexities of the native `WebApi` syntax. 8 | 9 | **Design Goals** 10 | 11 | - Implement an API for use from TypeScript that is close to `IOrganizationService` for use in Model-Driven Form JS Web-Resources or PCF controls. 12 | 13 | - Cross-Platform - pure NodeJS - runs inside VSCode on Windows/Mac/Linux 14 | 15 | - Early bound generation of Entities/Actions/Functions with customizable templates. 16 | 17 | - Be as unopinionated as possible - but still promote TypeScript best practices. 18 | 19 | - Allow integration testing from inside VSCode/NodeJS tests. 20 | 21 | See 22 | 23 | ## Adding `dataverse-ify` to your JavaScript Webresource TypeScript Project 24 | 25 | ### Authenticate against your environment 26 | 27 | `Dataverse-ify` uses a authentication token that is encrypted and stored on your machine using a library called `dataverse-auth`. 28 | 29 | At your VSCode command line run: 30 | 31 | ```shell 32 | npx dataverse-auth myorg.crm.dynamics.com 33 | ``` 34 | 35 | Replace `myorg.crm.dyanmics.com` with the URL of your tenant (without the `https://`) 36 | 37 | You will be prompted to login - if you have MFA enabled you will also need to provide the necessary details. 38 | 39 | Once you have logged in, a authentication token will be saved and you should not need to login again unless it expires (usually 90 days) or is invalidated (e.g. by your administrator making changes) 40 | 41 | ### Installing `Dataverse-ify` 42 | 43 | To use the `WebApi` using early-bound types you will need to install the `dataverse-ify` library using the following at your VSCode command line: 44 | 45 | ```shell 46 | npm install dataverse-ify --save 47 | ``` 48 | 49 | #### Generating early-bound types 50 | 51 | To initialise your early-bound type generation, use the following at the VSCode command line: 52 | 53 | ```shell 54 | npx dataverse-gen init 55 | ``` 56 | 57 | You will be asked to select from the environments you have authenticated against (if you have more than one). 58 | 59 | Select the Entities that you wish to generate types for. In this example we will select `account`, `opportunity`, `email` & `activityparty` 60 | 61 | 62 | Next you will be prompted to select the actions you wish to generate types for, we will select `WinOpportunity`. You can also select from any custom actions or custom APIs you have created. 63 | 64 | 65 | Lastly you are asked to select any functions – we will not select any just yet. 66 | 67 | You are then prompted if you want to generate the types: 68 | **Select Yes** 69 | 70 | ![d00ae9a1bdcd9741a805cb7f4cdb3cc1](media/Part 5 - Earlybound Types using dataverse-ify/d00ae9a1bdcd9741a805cb7f4cdb3cc1.png) 71 | 72 | In the root of your project you should now see a file called `.dataverse-gen.json` that looks like this: 73 | 74 | ```json 75 | { 76 | "entities": [ 77 | "account", 78 | "activityparty", 79 | "email", 80 | "opportunity" 81 | ], 82 | "actions": [ 83 | "WinOpportunity" 84 | ], 85 | "functions": [], 86 | "output": { 87 | "outputRoot": "./src/dataverse-gen" 88 | } 89 | } 90 | ``` 91 | 92 | There will also be a new folder called dataverse-gen under your src folder. This will contain the early-bound types and attribute constants. 93 | 94 | 95 | 96 | 97 | We can now update our `AccountForm.ts` with the attribute name constants `AccountAttributes.WebSiteURL` 98 | 99 | ```typescript 100 | static async onload(context: Xrm.Events.EventContext): Promise { 101 | context.getFormContext().getAttribute(AccountAttributes.WebSiteURL).addOnChange(AccountForm.onWebsiteChanged); 102 | } 103 | ``` 104 | 105 | VSCode should automatically import the `AccountAttributes` for you: 106 | 107 | ```typescript 108 | import { AccountAttributes } from "../dataverse-gen/entities/Account"; 109 | ``` 110 | 111 | Using this approach will result in less errors due to logical name inconsistencies. Additionally, if any attributes are deleted that are referenced by your code, when you re-run the dataverse-gen you will see build errors. 112 | 113 | You can re-generate the types at any time using: 114 | 115 | ```shell 116 | npx dataverse-gen 117 | ``` 118 | 119 | If you want to add to the metadata added, you can simply re-run: 120 | 121 | ```shell 122 | npx dataverse-gen init 123 | ``` 124 | 125 | or you can simply edit the `.dataverse-gen.json` file and run `npx dataverse-gen` 126 | 127 | ## Using custom templates! 128 | 129 | If you wanted to just generate Attribute `enum` constants and stop there, you can easily customise the scripts to suit your needs by using: 130 | 131 | ```shell 132 | npx dataverse-gen eject 133 | ``` 134 | 135 | This will create a step of templates ready to customise in the `_templates` folder. Once you have made your updates, just run `npx dataverse-gen` again. The templates use the awesome [ejs](https://ejs.co/) project. E.g. 136 | 137 | ```typescript 138 | // Attribute constants 139 | export const enum <%- locals.SchemaName %>Attributes { 140 | <%locals.Properties && locals.Properties.forEach(function(property){ _%> 141 | <%- property.SchemaName %> = "<%- property.Name %>", 142 | <%})_%> 143 | } 144 | ``` 145 | 146 | If you wanted to revert back to the standard templates, just delete the `_templates` folder 147 | 148 | ## Next Up 149 | 150 | Now that we've created early bound types, we will use `dataverse-ify` to call the `Xrm WebApi` with ease! 151 | 152 | -------------------------------------------------------------------------------- /Part 6 - Calling the WebApi.md: -------------------------------------------------------------------------------- 1 | # Part 6 - Calling the WebApi using dataverse-ify 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this sixth part we will cover calling the `WebApi` with easy using `dataverse-ify`. You can find full details about how the `WepApi` works at https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi. 6 | 7 | `Dataverse-ify` is an open source library that aims to provide an interface to the Dataverse `WebApi` from TypeScript that works in a similar way to the C\# `IOrganisationService` so that you do not need to code around the complexities of the native `WebApi` syntax. See 8 | 9 | ## Calling the `WebApi` using `CdsService` 10 | 11 | Now that you have generated the early-bound types, you can: 12 | 13 | - easily call the `WebApi` without the hassle of understanding the complexity it’s type system 14 | 15 | - perform integration testing against dataverse from inside VSCode 16 | 17 | We are going to create a Command Bar button to simply query all the opportunities regarding the account and then close each of them as Won. 18 | 19 | Although `dataverse-ify` provides an implementation of `CdsService` - you don't need to use it but instead use the `Xrm.WebApi` functions in combination with `odataify` and `sdkify`. For more information on the `dataverse-ify` implementation of `CdsService` and it's alternatives, see https://github.com/scottdurow/dataverse-ify/wiki/Using-dataverse%E2%80%90ify-without-the-CdsServiceClient 20 | 21 | ### Add the Ribbon Script to the Exports 22 | 23 | To your `src/index.ts` add: 24 | 25 | ```typescript 26 | export * from "./Ribbon/AccountRibbon"; 27 | ``` 28 | 29 | This will ensure that `AccountRibbon` class is available in the global scope along with the `AccountForm` class. If you don’t export the class, then it will only be accessible to internal code. This is a good way of ensuring you don’t overlap with any other libraries that are loaded. 30 | 31 | ### Add the `AccountRibbon.ts` 32 | 33 | Add a file `Ribbon/AccountRibbon.ts` 34 | 35 | ```typescript 36 | /* eslint-disable camelcase */ 37 | import { 38 | CdsServiceClient, 39 | EntityCollection, 40 | EntityReference, 41 | setMetadataCache, 42 | XrmContextCdsServiceClient, 43 | } from "dataverse-ify"; 44 | import { WinOpportunityMetadata, WinOpportunityRequest } from "../dataverse-gen/actions/WinOpportunity"; 45 | import { Opportunity } from "../dataverse-gen/entities/Opportunity"; 46 | import { opportunitycloseMetadata } from "../dataverse-gen/entities/OpportunityClose"; 47 | import { opportunity_opportunity_statuscode } from "../dataverse-gen/enums/opportunity_opportunity_statuscode"; 48 | import { metadataCache } from "../dataverse-gen/metadata"; 49 | 50 | export class AccountRibbon { 51 | static async closeOpporunities(context: Xrm.FormContext): Promise { 52 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 53 | await AccountRibbon.closeOpportunitiesInternal(serviceClient, context.data.entity.getId()); 54 | } 55 | static async closeOpportunitiesInternal(serviceClient: CdsServiceClient, accountid: string): Promise { 56 | setMetadataCache(metadataCache); 57 | // Get a list of all open opportunities 58 | const openOps = await serviceClient.retrieveMultiple(` 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | `); 68 | if (openOps.entities.length == 0) { 69 | await Xrm.Navigation.openAlertDialog({ text: `There are no open opportunties!` }); 70 | return; 71 | } 72 | const result = await Xrm.Navigation.openConfirmDialog({ 73 | title: "Close All Open Opportunities?", 74 | text: `Are you sure you want to close the ${openOps.entities.length} open opportunities?`, 75 | }); 76 | if (!result.confirmed) return; 77 | 78 | try { 79 | // For each Opportunity, Close as Won 80 | const opportunityCount = await AccountRibbon.closeOpportunities(openOps, serviceClient); 81 | Xrm.Utility.closeProgressIndicator(); 82 | await Xrm.Navigation.openAlertDialog({ title: "Success", text: `${opportunityCount} Opportunities closed` }); 83 | } catch (ex) { 84 | Xrm.Utility.closeProgressIndicator(); 85 | Xrm.Navigation.openErrorDialog({ 86 | details: ex, 87 | message: `Could not close opportunites:\n${ex.message}\n`, 88 | }); 89 | } 90 | } 91 | 92 | private static async closeOpportunities(openOps: EntityCollection, serviceClient: CdsServiceClient) { 93 | let opportunityCount = 0; 94 | for (const openOpportunity of openOps.entities) { 95 | opportunityCount++; 96 | Xrm.Utility.showProgressIndicator( 97 | `Closing Opportunity ${opportunityCount} of ${openOps.entities.length} - '${openOpportunity.name}', Please Wait...`, 98 | ); 99 | const winRequest = { 100 | logicalName: WinOpportunityMetadata.operationName, 101 | Status: opportunity_opportunity_statuscode.Won, 102 | OpportunityClose: { 103 | logicalName: opportunitycloseMetadata.logicalName, 104 | subject: "Opportunity Won", 105 | opportunityid: new EntityReference(openOpportunity.logicalName, openOpportunity.opportunityid), 106 | }, 107 | } as WinOpportunityRequest; 108 | await serviceClient.execute(winRequest); 109 | } 110 | return opportunityCount; 111 | } 112 | } 113 | 114 | ``` 115 | 116 | Note that we are exporting a class with static methods - if you preferred you could simply export functions. I find that classes are a convenient way of naming a module export when you want to group together related functionality that is called by a Model Driven App. Of course, in other places you will be using classes in the conventional way! 117 | 118 | The first thing you might notice is there is an async function called `closeOpportunitiesInternal` that is called from a non-async function `closeOpporunities`. This is a common technique to provide an external function that accepts the parameters passed via the Ribbon (the `PrimaryControl` Form context in this case), and then an internal implementation that can be used in Unit Tests that accepts the services that are mockable. This is an alternative to a technique called `inversion of control`. 119 | 120 | The key part of this code is: 121 | 122 | ```typescript 123 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 124 | ``` 125 | 126 | This is creating an instance of `dataverse-ify's` service client that isolates you from the complexity of the WebApi and provides a very similar API to the C# SDK `IOrganizationService`. 127 | 128 | Before you can use it however, you must make a call to: 129 | 130 | ```typescript 131 | setMetadataCache(metadataCache); 132 | ``` 133 | 134 | This adds the additional information about the early-bound types generated earlier using dataverse-gen. For more info - see https://github.com/scottdurow/dataverse-ify/wiki/Why-generate-metadata%3F 135 | 136 | Notice that we have a simple interface for `WinOpportunityRequest` that provides the necessarily information - greatly simplifying the different ways that the `WebApi` accepts parameters and types. If you were creating activities, you will also find that the `dataverse-ify` types simplify other complex areas such as `activity parties`. 137 | 138 | ## Adding Unit Tests 139 | 140 | Update `package.json` with integration and unit tests 141 | 142 | ```json 143 | "scripts": { 144 | "build": "webpack --config webpack.dev.js", 145 | "start": "webpack --config webpack.dev.js --watch", 146 | "dist": "webpack --config webpack.prod.js", 147 | "test": "jest unit.", 148 | "integrationtests": "jest integration." 149 | }, 150 | ``` 151 | 152 | This allows us to run integration tests separately from unit tests since they are very different in nature. 153 | 154 | ### Create Mock `ServiceClient` 155 | 156 | One of the principles of unit testing is that we only want to exercise the code we want to test. For this reason, we mock other code units so that we can control the behaviour and monitor calls. 157 | 158 | The `dataverse-ify` `CdsServiceClient` is an interface that can be implemented in order to add mock implementations using jest. 159 | 160 | Add a new file `src/Mocks/MockServiceClient.ts` 161 | 162 | To this file, add the code found at: 163 | 164 | You can see that this class does nothing other than implement the `CdsServiceClient` interface. Later we will use jest to add mock implementations and expectations. Code such as this that is used only by our unit tests will not be exported or included in the webpack bundle created for our JavaScript webresource. 165 | 166 | ### Add the AccountRibbon Unit tests 167 | 168 | Add a new file `src\Ribbon\__tests__\unit.AccountRibbon.test.ts` 169 | 170 | To this file add the code: 171 | 172 | ```typescript 173 | /* eslint-disable camelcase */ 174 | import { EntityCollection, EntityReference } from "dataverse-ify"; 175 | import { XrmMockGenerator } from "xrm-mock"; 176 | import { WinOpportunityRequest } from "../../dataverse-gen/actions/WinOpportunity"; 177 | import { Opportunity, opportunityMetadata } from "../../dataverse-gen/entities/Opportunity"; 178 | import { OpportunityClose, opportunitycloseMetadata } from "../../dataverse-gen/entities/OpportunityClose"; 179 | import { opportunity_opportunity_statuscode } from "../../dataverse-gen/enums/opportunity_opportunity_statuscode"; 180 | import { MockServiceClient } from "../../Mocks/MockServiceClient"; 181 | import { AccountRibbon } from "../AccountRibbon"; 182 | 183 | describe("AccountRibbon", () => { 184 | beforeEach(() => { 185 | XrmMockGenerator.initialise(); 186 | Xrm.Utility.showProgressIndicator = jest.fn(); 187 | Xrm.Utility.closeProgressIndicator = jest.fn(); 188 | Xrm.Navigation.openAlertDialog = jest.fn(); 189 | Xrm.Navigation.openErrorDialog = jest.fn().mockImplementation((ex) => console.debug(JSON.stringify(ex))); 190 | }); 191 | 192 | it("closes open opportunities", async () => { 193 | // Arrange 194 | const mockServiceClient = new MockServiceClient(); 195 | 196 | mockServiceClient.retrieveMultiple = jest.fn().mockImplementation(() => { 197 | return new EntityCollection([ 198 | { 199 | logicalName: opportunityMetadata.logicalName, 200 | opportunityid: "111", 201 | name: "AAA", 202 | } as Opportunity, 203 | ]); 204 | }); 205 | mockServiceClient.execute = jest.fn(); 206 | Xrm.Navigation.openConfirmDialog = jest.fn().mockReturnValue({ confirmed: true } as Xrm.Navigation.ConfirmResult); 207 | 208 | // Act 209 | await AccountRibbon.closeOpportunitiesInternal(mockServiceClient, "222"); 210 | 211 | // Assert 212 | expect(Xrm.Navigation.openErrorDialog).toHaveBeenCalledTimes(0); 213 | expect(mockServiceClient.execute).toHaveBeenCalledWith( 214 | expect.objectContaining({ 215 | Status: opportunity_opportunity_statuscode.Won, 216 | OpportunityClose: expect.objectContaining({ 217 | logicalName: opportunitycloseMetadata.logicalName, 218 | opportunityid: new EntityReference(opportunityMetadata.logicalName, "111"), 219 | } as OpportunityClose), 220 | } as WinOpportunityRequest), 221 | ); 222 | }); 223 | }); 224 | ``` 225 | 226 | The key part of this test is that it first arranges the mock implementations needed. `beforeEach` is run before each `it()` test and calls `xrm-mock` to create a mock `Xrm` context in a similar way to when we were testing the `AccountForm` code. 227 | 228 | We also mock the calls to Navigation methods. The following will simulate the user clicking OK on the confirmation dialog. 229 | 230 | ```typescript 231 | Xrm.Navigation.openConfirmDialog = jest.fn().mockReturnValue({ confirmed: true } as Xrm.Navigation.ConfirmResult); 232 | ``` 233 | 234 | Because we are calling the `WebApi.retrieveMultiple` and `WebApi.execute`, we also have to mock these functions. The `retreiveMultiple` call simulates a single opportunity being returned. 235 | 236 | ```typescript 237 | mockServiceClient.retrieveMultiple = jest.fn().mockImplementation(() => { 238 | return new EntityCollection([ 239 | { 240 | logicalName: opportunityMetadata.logicalName, 241 | opportunityid: "111", 242 | name: "AAA", 243 | } as Opportunity, 244 | ]); 245 | }); 246 | ``` 247 | 248 | The execute call is simply a jest mock function that allows us to assert that the right calls were made. 249 | 250 | ```typescript 251 | mockServiceClient.execute = jest.fn(); 252 | ``` 253 | 254 | ### Expectations 255 | 256 | Once we have called the `AccountRibbon.closeOpportunitiesInternal` we can call the expectations to ensure that the execute method was called with the correct arguments: 257 | 258 | ```typescript 259 | expect(mockServiceClient.execute).toHaveBeenCalledWith( 260 | expect.objectContaining({ 261 | Status: opportunity_opportunity_statuscode.Won, 262 | OpportunityClose: expect.objectContaining({ 263 | logicalName: opportunitycloseMetadata.logicalName, 264 | opportunityid: new EntityReference(opportunityMetadata.logicalName, "111"), 265 | } as OpportunityClose), 266 | } as WinOpportunityRequest), 267 | ); 268 | ``` 269 | 270 | You can run your unit tests in the same way we did in Part 3, either by pressing F5 with the unit test file open, or at the command line: 271 | 272 | ```shell 273 | jest unit.AccountRibbon 274 | ``` 275 | 276 | ## Adding Integration Tests 277 | 278 | Unit tests will enable you to quickly ensure that your code is functioning as expected - however the one unknown is how Dataverse will behave when calling the `WebApi`. Mocking assumes that it returns what you are expected, however an integration test will ensure it functions according to those expectations and can be a very effective way of ensure your code covers all scenarios and edge-cases. The principle of an integration test is that it should create the data necessary to run the test, and then delete it afterwards so that it leaves no footprint. 279 | 280 | ### Calling Dataverse from inside VSCode 281 | 282 | `dataverse-ify` contains an implementation of the `WebApi` that allows you to use the `Xrm.WebApi` API from inside your VSCode integration tests running locally without debugging inside the browser. This can reduce the amount of time it takes to develop and debug your code. 283 | 284 | Inside your VSCode project, add an file `config\test.yaml` 285 | 286 | ```yaml 287 | nodewebapi: 288 | logging: verbose 289 | server: 290 | host: https://org.crm.dynamics.com 291 | version: 9.1 292 | ``` 293 | 294 | Replace `org.crm.dynamics.com` with the URL of your environment. 295 | 296 | ### Add Integration Test 297 | 298 | Now we have configured connectivity to our Dataverse environment, we can create a integration test similar to our unit test, however this one will actually communicate with the server. 299 | 300 | Add a new file `src\Ribbon\__tests__\integration.AccountRibbon.test.ts` 301 | 302 | ```typescript 303 | /* eslint-disable camelcase */ 304 | import { XrmMockGenerator } from "xrm-mock"; 305 | import { SetupGlobalContext } from "dataverse-ify/lib/webapi"; 306 | import { Opportunity, OpportunityAttributes, opportunityMetadata } from "../../dataverse-gen/entities/Opportunity"; 307 | import { Account, accountMetadata } from "../../dataverse-gen/entities/Account"; 308 | import { Entity, setMetadataCache, XrmContextCdsServiceClient } from "dataverse-ify"; 309 | import { AccountRibbon } from "../AccountRibbon"; 310 | import { opportunity_opportunity_statecode } from "../../dataverse-gen/enums/opportunity_opportunity_statecode"; 311 | import { metadataCache } from "../../dataverse-gen/metadata"; 312 | describe("AccountRibbon", () => { 313 | beforeEach(async () => { 314 | XrmMockGenerator.initialise(); 315 | await SetupGlobalContext(); 316 | setMetadataCache(metadataCache); 317 | Xrm.Utility.showProgressIndicator = jest.fn(); 318 | Xrm.Utility.closeProgressIndicator = jest.fn(); 319 | Xrm.Navigation.openAlertDialog = jest.fn(); 320 | Xrm.Navigation.openErrorDialog = jest.fn().mockImplementation((ex) => console.debug(JSON.stringify(ex))); 321 | Xrm.Navigation.openConfirmDialog = jest.fn().mockReturnValue({ confirmed: true } as Xrm.Navigation.ConfirmResult); 322 | }); 323 | 324 | it("closes open opportunities", async () => { 325 | // Arrange 326 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 327 | 328 | const opportunity1 = { 329 | logicalName: opportunityMetadata.logicalName, 330 | name: "Opportunity Integration Test", 331 | } as Opportunity; 332 | 333 | const account1 = { 334 | logicalName: accountMetadata.logicalName, 335 | name: "Account Integration Test", 336 | } as Account; 337 | 338 | // Act 339 | try { 340 | account1.id = await serviceClient.create(account1); 341 | opportunity1.customerid = Entity.toEntityReference(account1); 342 | opportunity1.id = await serviceClient.create(opportunity1); 343 | 344 | await AccountRibbon.closeOpportunitiesInternal(serviceClient, account1.id); 345 | 346 | // Assert 347 | // Check that the opportunity is closed 348 | const closedOp = await serviceClient.retrieve(opportunityMetadata.logicalName, opportunity1.id, [ 349 | OpportunityAttributes.StateCode, 350 | OpportunityAttributes.StatusCode, 351 | ]); 352 | expect(closedOp.statecode).toBe(opportunity_opportunity_statecode.Won); 353 | } catch (ex) { 354 | fail(ex); 355 | } finally { 356 | // Tidy up 357 | if (opportunity1.id) await serviceClient.delete(opportunity1); 358 | if (account1.id) await serviceClient.delete(account1); 359 | } 360 | }, 100000); 361 | }); 362 | 363 | ``` 364 | 365 | > **Note** The 100000 at the end of the test code is the timeout in milliseconds that we allow for the test to run. 366 | 367 | ### SetupGlobalContext 368 | 369 | The key part to our integration test is the call to `await SetupGlobalContext();`This is a function that is imported via `import { SetupGlobalContext } from "dataverse-ify/lib/webapi"` and is responsible for replacing the `Xrm.WebApi` implementation locally to point to an implementation that uses the `config/test.yaml` file and call the corresponding Dataverse environment. You don't need to be using `Dataverse-ify's` `CdsService` to use this - it works for normal `Xrm.WebApi` calls. 370 | 371 | > **Note:** `SetupGlobalContext` only ever needs to be called inside your unit tests, however `setMetadataCache` will need to be called before you make calls to `dataverse-ify` even in your web resource code. 372 | 373 | ### Asserting integration test results 374 | 375 | In our integration tests, we can easily query dataverse to check that the necessary operations have been carried out. This is similar to the mock exceptions we added to our unit tests earlier. These expectations use the `serviceClient` in a similar way to the code we are testing does! 376 | 377 | ```typescript 378 | const closedOp = await serviceClient.retrieve(opportunityMetadata.logicalName, opportunity1.id, [ 379 | OpportunityAttributes.StateCode, 380 | OpportunityAttributes.StatusCode, 381 | ]); 382 | expect(closedOp.statecode).toBe(opportunity_opportunity_statecode.Won); 383 | ``` 384 | 385 | ## Running Integration tests 386 | 387 | Once you have written your integration tests, you can debug using F5 (with the test open) in the same way that you did for the unit tests. This has the advantage that you can check how your code works against your dataverse environment without continuously setting up records in the user interface. Once your integration tests work inside jest, you should have a high degree of confidence that the code will work once deployed to the Model Driven App! 388 | 389 | Once you have debugged your tests, you can run them all using: 390 | 391 | ```shell 392 | jest interation 393 | ``` 394 | 395 | ## Next Up 396 | 397 | Now that we've created unit tests we can try our code against dataverse using an integration tests - right from inside VSCode! 398 | 399 | -------------------------------------------------------------------------------- /Part 7 - Integration Tests with the WebApi.md: -------------------------------------------------------------------------------- 1 | # Part 7 - Integration tests with `dataverse-ify` 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this seventh part we will cover calling the `WebApi` with easy using `dataverse-ify`. You can find full details about how the `WepApi` works at https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi. 6 | 7 | `Dataverse-ify` is an open source library that aims to provide an interface to the Dataverse `WebApi` from TypeScript that works in a similar way to the C\# `IOrganisationService` so that you do not need to code around the complexities of the native `WebApi` syntax. See 8 | 9 | 10 | ## Adding Integration Tests 11 | 12 | Unit tests will enable you to quickly ensure that your code is functioning as expected - however the one unknown is how Dataverse will behave when calling the `WebApi`. Mocking assumes that it returns what you are expected, however an integration test will ensure it functions according to those expectations and can be a very effective way of ensure your code covers all scenarios and edge-cases. The principle of an integration test is that it should create the data necessary to run the test, and then delete it afterwards so that it leaves no footprint. 13 | 14 | ### Calling Dataverse from inside VSCode 15 | 16 | `dataverse-ify` contains an implementation of the `WebApi` that allows you to use the `Xrm.WebApi` API from inside your VSCode integration tests running locally without debugging inside the browser. This can reduce the amount of time it takes to develop and debug your code. 17 | 18 | Inside your VSCode project, add an file `config\test.yaml` 19 | 20 | ```yaml 21 | nodewebapi: 22 | logging: verbose 23 | server: 24 | host: https://org.crm.dynamics.com 25 | version: 9.1 26 | ``` 27 | 28 | Replace `org.crm.dynamics.com` with the URL of your environment. 29 | 30 | ### Add Integration Test 31 | 32 | Now we have configured connectivity to our Dataverse environment, we can create a integration test similar to our unit test, however this one will actually communicate with the server. 33 | 34 | Add a new file `src\Ribbon\__tests__\integration.AccountRibbon.test.ts` 35 | 36 | ```typescript 37 | /* eslint-disable camelcase */ 38 | import { XrmMockGenerator } from "xrm-mock"; 39 | import { SetupGlobalContext } from "dataverse-ify/lib/webapi"; 40 | import { Opportunity, OpportunityAttributes, opportunityMetadata } from "../../dataverse-gen/entities/Opportunity"; 41 | import { Account, accountMetadata } from "../../dataverse-gen/entities/Account"; 42 | import { Entity, setMetadataCache, XrmContextCdsServiceClient } from "dataverse-ify"; 43 | import { AccountRibbon } from "../AccountRibbon"; 44 | import { opportunity_opportunity_statecode } from "../../dataverse-gen/enums/opportunity_opportunity_statecode"; 45 | import { metadataCache } from "../../dataverse-gen/metadata"; 46 | describe("AccountRibbon", () => { 47 | beforeEach(async () => { 48 | XrmMockGenerator.initialise(); 49 | await SetupGlobalContext(); 50 | setMetadataCache(metadataCache); 51 | Xrm.Utility.showProgressIndicator = jest.fn(); 52 | Xrm.Utility.closeProgressIndicator = jest.fn(); 53 | Xrm.Navigation.openAlertDialog = jest.fn(); 54 | Xrm.Navigation.openErrorDialog = jest.fn().mockImplementation((ex) => console.debug(JSON.stringify(ex))); 55 | Xrm.Navigation.openConfirmDialog = jest.fn().mockReturnValue({ confirmed: true } as Xrm.Navigation.ConfirmResult); 56 | }); 57 | 58 | it("closes open opportunities", async () => { 59 | // Arrange 60 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 61 | 62 | const opportunity1 = { 63 | logicalName: opportunityMetadata.logicalName, 64 | name: "Opportunity Integration Test", 65 | } as Opportunity; 66 | 67 | const account1 = { 68 | logicalName: accountMetadata.logicalName, 69 | name: "Account Integration Test", 70 | } as Account; 71 | 72 | // Act 73 | try { 74 | account1.id = await serviceClient.create(account1); 75 | opportunity1.customerid = Entity.toEntityReference(account1); 76 | opportunity1.id = await serviceClient.create(opportunity1); 77 | 78 | await AccountRibbon.closeOpportunitiesInternal(serviceClient, account1.id); 79 | 80 | // Assert 81 | // Check that the opportunity is closed 82 | const closedOp = await serviceClient.retrieve(opportunityMetadata.logicalName, opportunity1.id, [ 83 | OpportunityAttributes.StateCode, 84 | OpportunityAttributes.StatusCode, 85 | ]); 86 | expect(closedOp.statecode).toBe(opportunity_opportunity_statecode.Won); 87 | } catch (ex) { 88 | fail(ex); 89 | } finally { 90 | // Tidy up 91 | if (opportunity1.id) await serviceClient.delete(opportunity1); 92 | if (account1.id) await serviceClient.delete(account1); 93 | } 94 | }, 100000); 95 | }); 96 | 97 | ``` 98 | 99 | > **Note** The 100000 at the end of the test code is the timeout in milliseconds that we allow for the test to run. 100 | 101 | ### `SetupGlobalContext` 102 | 103 | The key part to our integration test is the call to `await SetupGlobalContext();`This is a function that is imported via `import { SetupGlobalContext } from "dataverse-ify/lib/webapi"` and is responsible for replacing the `Xrm.WebApi` implementation locally to point to an implementation that uses the `config/test.yaml` file and call the corresponding Dataverse environment. You don't need to be using `Dataverse-ify's` `CdsService` to use this - it works for normal `Xrm.WebApi` calls. 104 | 105 | > **Note:** `SetupGlobalContext` only ever needs to be called inside your unit tests, however `setMetadataCache` will need to be called before you make calls to `dataverse-ify` even in your web resource code. 106 | 107 | ### Asserting integration test results 108 | 109 | In our integration tests, we can easily query dataverse to check that the necessary operations have been carried out. This is similar to the mock exceptions we added to our unit tests earlier. These expectations use the `serviceClient` in a similar way to the code we are testing does! 110 | 111 | ```typescript 112 | const closedOp = await serviceClient.retrieve(opportunityMetadata.logicalName, opportunity1.id, [ 113 | OpportunityAttributes.StateCode, 114 | OpportunityAttributes.StatusCode, 115 | ]); 116 | expect(closedOp.statecode).toBe(opportunity_opportunity_statecode.Won); 117 | ``` 118 | 119 | ## Running Integration tests 120 | 121 | Once you have written your integration tests, you can debug using F5 (with the test open) in the same way that you did for the unit tests. This has the advantage that you can check how your code works against your dataverse environment without continuously setting up records in the user interface. Once your integration tests work inside jest, you should have a high degree of confidence that the code will work once deployed to the Model Driven App! 122 | 123 | Once you have debugged your tests, you can run them all using: 124 | 125 | ```shell 126 | jest interation 127 | ``` 128 | 129 | ## Next Up 130 | 131 | Now that we've created some fairly complex JavaScript logic and tested it (unit and integration) we are ready to deploy and test inside our Model Driven App. 132 | 133 | -------------------------------------------------------------------------------- /Part 8 - Calling JavaScript from a Command Bar Button.md: -------------------------------------------------------------------------------- 1 | # Part 8 - Calling JavaScript webresources from the Command Bar 2 | 3 | This is part of the course 'Scott's guide to building Power Apps JavaScript Web Resources using TypeScript'. 4 | 5 | In this eighth part we will cover how to create a button on the Ribbon to call the JavaScript webresource that we have created. In a later part of this series we will convert this to call a Custom API. 6 | 7 | 8 | ## Add Command Bar Button using the Ribbon Workbench 9 | 10 | Before we can test our code inside the Model Driven App, we must configure the Command Bar to have a button that calls our AccountRibbon code. 11 | 12 | 1. Open the Ribbon Workbench (see https://ribbonworkbench.uservoice.com/knowledgebase/articles/71374-1-getting-started-with-the-ribbon-workbench) 13 | 14 | 2. Load a solution that contains just the account table/entity - see https://ribbonworkbench.uservoice.com/knowledgebase/articles/169819-speed-up-publishing-of-solutions 15 | 16 | 3. Create a new Display Rule: 17 | 18 | 19 | 4. Add a new Step to the Enable Rule of type `FormStateRule`. Set the State Property to `Existing`. This will be used to ensure our button is only visible on existing records. 20 | 21 | 5. Add a new Command and add the Display Rule: 22 | 23 | 24 | 6. To the Command add a Custom JavaScript Action 25 | 26 | 7. Set the Command Properties as follows: 27 | 28 | - **Library:** `\$webresource:dev1_/js/clienthooks.js` 29 | 30 | - **Function Name:** `cds.ClientHooks.AccountRibbon.closeOpporunities` 31 | 32 | The name of the Function is very important that it matches the name you have exported. The first part will be the namespace you set in the `webpack.common.js` `library` property, and the second part will be the name of the class you exported followed by the static method. 33 | 34 | 8. Add a `Crm Parameter` of Type `PrimaryControl` - this is so that the form context is passed to `closeOpporunities(context: Xrm.FormContext)` 35 | 36 | 9. Drag a new button on to the Account Form Command Bar and set the Command to be the one you just created. Give the button a name such as 'Close Opportunities' and possibly a `ModernImage`. 37 | 38 | 39 | 10. Publish your changes in the Ribbon Workbench. 40 | 41 | ## Testing in the Browser 42 | 43 | 1. Build your VSCode project using `npm start` and ensure Fiddler is running with auto-responders turned on as we did in Part 4. 44 | 45 | 2. Press F12 to open the Developer Tools (or use `Ctrl+Shift+I`) 46 | 47 | 3. Open an Account record that has some open Opportunities. You should see your new Ribbon Button. 48 | **Note:** Using integration tests has a distinct advantage over in-browser testing since you can automatically create records that are needed to execute the tests rather than doing it manually each time. 49 | 50 | 4. In the Developer Tools, search for the file `webpack://AccountRibbon.ts` and place a breakpoint in the `closeOpportunitiesInternal` method. 51 | 52 | 5. Press the Command Bar button and you should see your code hitting the breakpoint and be able to step through and debug. 53 | 54 | 6. If you make any changes in VSCode you simply need to refresh the record and you will get the re-built version because of the Fiddler auto-responder. 55 | 56 | image-20210516124244333 57 | 58 | ### Troubleshooting 59 | 60 | #### setMetadataCache not called 61 | 62 | If you get the following error ‘Error: Metadata not found for opportunity. Please create the early bound types.’ This usually means you have forgotten to call `setMetadataCache(metadataCache);` 63 | 64 | #### Missing await 65 | 66 | If you receive ‘UnhandledPromiseRejectionWarning: Error: Caught error after test environment was torn down’ or ‘Jest did not exit one second after the test run has completed.’ – it usually means that you have called an `async` function in your test that returns a `Promise`, but you have not used `await`. 67 | 68 | ## Next Up 69 | 70 | Now that we've created some fairly complex JavaScript logic - we will look at moving some of this logic to the server via a Custom API - and then invoking that Custom API using TypeScript and `dataverse-ify` early-bound types. 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Scott's guide to building Power Apps JavaScript Web Resources using TypeScript! 2 | 3 | This repo is the source for the video instruction course: https://learn.develop1.net/courses/building-javascript-web-resources-using-typescript 4 | -------------------------------------------------------------------------------- /code/.gitignore: -------------------------------------------------------------------------------- 1 |  2 | # Created by https://www.gitignore.io/api/visualstudio 3 | # Edit at https://www.gitignore.io/?templates=visualstudio 4 | 5 | ### VisualStudio ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUNIT 51 | *.VisualState.xml 52 | TestResult.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # JustCode is a .NET coding add-in 134 | .JustCode 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # The packages folder can be ignored because of Package Restore 194 | **/[Pp]ackages/* 195 | # except build/, which is used as an MSBuild target. 196 | !**/[Pp]ackages/build/ 197 | # Uncomment if necessary however generally it will be regenerated when needed 198 | #!**/[Pp]ackages/repositories.config 199 | # NuGet v3's project.json files produces more ignorable files 200 | *.nuget.props 201 | *.nuget.targets 202 | 203 | # Microsoft Azure Build Output 204 | csx/ 205 | *.build.csdef 206 | 207 | # Microsoft Azure Emulator 208 | ecf/ 209 | rcf/ 210 | 211 | # Windows Store app package directories and files 212 | AppPackages/ 213 | BundleArtifacts/ 214 | Package.StoreAssociation.xml 215 | _pkginfo.txt 216 | *.appx 217 | *.appxbundle 218 | *.appxupload 219 | 220 | # Visual Studio cache files 221 | # files ending in .cache can be ignored 222 | *.[Cc]ache 223 | # but keep track of directories ending in .cache 224 | !?*.[Cc]ache/ 225 | 226 | # Others 227 | ClientBin/ 228 | ~$* 229 | *~ 230 | *.dbmdl 231 | *.dbproj.schemaview 232 | *.jfm 233 | *.pfx 234 | *.publishsettings 235 | orleans.codegen.cs 236 | 237 | # Including strong name files can present a security risk 238 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 239 | #*.snk 240 | 241 | # Since there are multiple workflows, uncomment next line to ignore bower_components 242 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 243 | #bower_components/ 244 | 245 | # RIA/Silverlight projects 246 | Generated_Code/ 247 | 248 | # Backup & report files from converting an old project file 249 | # to a newer Visual Studio version. Backup files are not needed, 250 | # because we have git ;-) 251 | _UpgradeReport_Files/ 252 | Backup*/ 253 | UpgradeLog*.XML 254 | UpgradeLog*.htm 255 | ServiceFabricBackup/ 256 | *.rptproj.bak 257 | 258 | # SQL Server files 259 | *.mdf 260 | *.ldf 261 | *.ndf 262 | 263 | # Business Intelligence projects 264 | *.rdl.data 265 | *.bim.layout 266 | *.bim_*.settings 267 | *.rptproj.rsuser 268 | *- Backup*.rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # End of https://www.gitignore.io/api/visualstudio 352 | 353 | Log*.txt 354 | -------------------------------------------------------------------------------- /code/AddApplicationUserToEnv.ps1: -------------------------------------------------------------------------------- 1 | Install-Module -Name AzureAD -AllowClobber -Scope CurrentUser 2 | Install-Module -Name Microsoft.Xrm.OnlineManagementAPI -MaximumVersion 1.2.0.1 -Scope CurrentUser 3 | Install-Module -Name Microsoft.Xrm.Data.Powershell -Scope CurrentUser -AllowClobber 4 | 5 | $appId = [Guid]"61bebcd1-d430-4a77-a77e-ed58df823453" 6 | 7 | 8 | # Create the Application User 9 | Connect-CrmOnlineDiscovery -InteractiveMode 10 | $userid= New-CrmRecord -EntityLogicalName systemuser -Fields @{ 11 | "applicationid"=$appId; 12 | "businessunitid"=(New-CrmEntityReference -Id (Invoke-CrmWhoAmI).BusinessUnitId -EntityLogicalName systemuser) 13 | } 14 | Write-Host "Application User Created with ID: $userid" 15 | 16 | 17 | # Add the Administrator Role 18 | 19 | $users = Get-CrmRecords -EntityLogicalName systemuser -FilterAttribute systemuserid -FilterOperator eq -FilterValue $userid -Fields "businessunitid" 20 | 21 | $SecurityRoleName = "System Administrator" 22 | $systemUserId = $users.CrmRecords[0].systemuserid 23 | $businessUnitId = $users.CrmRecords[0].businessunitid_Property.Value.Id 24 | 25 | 26 | $fetch = @" 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | "@ 37 | 38 | $fetch = $fetch -F $SecurityRoleName, $businessUnitId 39 | $securityRole = Get-CrmRecordsByFetch -Fetch $fetch 40 | 41 | 42 | If($securityRole.CrmRecords.Count -eq 0) 43 | { 44 | Write-Error "SecurityRole $SecurityRoleName does not exist" 45 | return 46 | } 47 | Else 48 | { 49 | Write-Output "Assign $SecurityRoleName role to user" 50 | $securityRoleId = $securityRole.CrmRecords[0].roleid.Guid 51 | Add-CrmSecurityRoleToUser -UserId $systemUserId -SecurityRoleId $securityRoleId 52 | } 53 | 54 | -------------------------------------------------------------------------------- /code/DataverseSolution/DataverseSolution.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {9CC37FDC-E3E7-40D7-A695-889731365097} 8 | Library 9 | Properties 10 | DataverseSolution 11 | DataverseSolution 12 | v4.6.2 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /code/DataverseSolution/DataverseSolution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30523.141 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataverseSolution", "DataverseSolution.csproj", "{9CC37FDC-E3E7-40D7-A695-889731365097}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9CC37FDC-E3E7-40D7-A695-889731365097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {9CC37FDC-E3E7-40D7-A695-889731365097}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {9CC37FDC-E3E7-40D7-A695-889731365097}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {9CC37FDC-E3E7-40D7-A695-889731365097}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {4927FD70-B260-429C-A17A-7A32872F975D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /code/DataverseSolution/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /code/DataverseSolution/spkl.json: -------------------------------------------------------------------------------- 1 | { 2 | "webresources": [ 3 | { 4 | 5 | "profile": "default,debug", 6 | "solution": "supersolution", 7 | "files": [ 8 | { 9 | "uniquename": "dev1_/js/clienthooks.js", 10 | "file": "../clientjs/dist/clienthooks.js", 11 | "description": "JavaScript for Forms and Commandbar Actions" 12 | } 13 | ] 14 | } 15 | ], 16 | "solutions": [ 17 | { 18 | "profile": "default,debug", 19 | "solution_uniquename": "supersolution", 20 | "packagepath": "package", 21 | "solutionpath": "solution_{0}_{1}_{2}_{3}.zip", 22 | "packagetype": "unmanaged", 23 | "increment_on_import": false, 24 | "map": [ 25 | { 26 | "map": "file", 27 | "from": "Webresources/dev1_/js/clienthooks.js", 28 | "to": "../../clientjs/dist/clienthooks.js" 29 | } 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/deploy-plugins.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl plugins [path] [connection-string] [/p:release] 12 | "%spkl_path%" plugins "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/deploy-webresources.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl webresources [path] [connection-string] 12 | "%spkl_path%" webresources "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/deploy-workflows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl workflow [path] [connection-string] [/p:release] 12 | "%spkl_path%" workflow "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/download-webresources.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl instrument [path] [connection-string] [/p:release] 12 | "%spkl_path%" download-webresources "%cd%\.." /o %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/earlybound.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl earlybound [path] [connection-string] [/p:release] 12 | "%spkl_path%" earlybound "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/instrument-plugin-code.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl instrument [path] [connection-string] [/p:release] 12 | "%spkl_path%" instrument "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/pack+import.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl instrument [path] [connection-string] [/p:release] 12 | "%spkl_path%" import "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/packonly.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl instrument [path] [connection-string] [/p:release] 12 | "%spkl_path%" pack "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/DataverseSolution/spkl/unpack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set package_root=..\..\ 3 | REM Find the spkl in the package folder (irrespective of version) 4 | For /R %package_root% %%G IN (spkl.exe) do ( 5 | IF EXIST "%%G" (set spkl_path=%%G 6 | goto :continue) 7 | ) 8 | 9 | :continue 10 | @echo Using '%spkl_path%' 11 | REM spkl instrument [path] [connection-string] [/p:release] 12 | "%spkl_path%" unpack "%cd%\.." %* 13 | 14 | if errorlevel 1 ( 15 | echo Error Code=%errorlevel% 16 | exit /b %errorlevel% 17 | ) 18 | 19 | pause -------------------------------------------------------------------------------- /code/clientjs/.dataverse-gen.json: -------------------------------------------------------------------------------- 1 | { 2 | "entities": [ 3 | "account", 4 | "activityparty", 5 | "email", 6 | "opportunity" 7 | ], 8 | "actions": [ 9 | "WinOpportunity" 10 | ], 11 | "functions": [], 12 | "output": { 13 | "outputRoot": "./src/dataverse-gen" 14 | } 15 | } -------------------------------------------------------------------------------- /code/clientjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "jest": true, 8 | "jasmine": true 9 | }, 10 | "extends": [ 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:react/recommended", 14 | "prettier" 15 | ], 16 | "parserOptions": { 17 | "project": "./tsconfig.json" 18 | }, 19 | "settings": { 20 | "react": { 21 | "pragma": "React", 22 | "version": "detect" 23 | } 24 | }, 25 | "plugins": [ 26 | "@typescript-eslint", 27 | "prettier" 28 | ], 29 | "rules": { 30 | "prettier/prettier": "error" 31 | }, 32 | "overrides": [ 33 | { 34 | "files": ["*.ts"], 35 | "rules": { 36 | "camelcase": [2, { "properties": "never" }] 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /code/clientjs/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "endOfLine":"auto" 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | // Use IntelliSense to learn about possible attributes. 4 | // Hover to view descriptions of existing attributes. 5 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "node", 10 | "name": "vscode-jest-tests", 11 | "request": "launch", 12 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 13 | "args": [ 14 | "--runInBand", 15 | "--code-coverage=false", 16 | "--runTestsByPath", 17 | "--testPathPattern=${fileBasename}" 18 | ], 19 | "cwd": "${workspaceFolder}", 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen", 22 | "disableOptimisticBPs": true, 23 | "smartStep": true, 24 | "skipFiles": [ 25 | "node_modules/**/*.js", 26 | "/**/*.js", 27 | "async_hooks.js", 28 | "inspector_async_hook.js" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /code/clientjs/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": false 4 | } 5 | } -------------------------------------------------------------------------------- /code/clientjs/config/test.yaml: -------------------------------------------------------------------------------- 1 | nodewebapi: 2 | logging: verbose 3 | server: 4 | host: https://dev1demos14.crm3.dynamics.com 5 | version: 9.1 -------------------------------------------------------------------------------- /code/clientjs/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | }; 5 | -------------------------------------------------------------------------------- /code/clientjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clientjs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.dev.js", 8 | "start": "webpack --config webpack.dev.js --watch", 9 | "dist": "webpack --config webpack.prod.js", 10 | "test": "jest unit.", 11 | "integrationtests": "jest integration." 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/jest": "^26.0.23", 17 | "@types/xrm": "^9.0.39", 18 | "@typescript-eslint/eslint-plugin": "^4.22.1", 19 | "@typescript-eslint/parser": "^4.22.1", 20 | "eslint": "^7.25.0", 21 | "eslint-config-prettier": "^8.3.0", 22 | "eslint-plugin-prettier": "^3.4.0", 23 | "eslint-plugin-react": "^7.23.2", 24 | "jest": "^26.6.3", 25 | "prettier": "2.2.1", 26 | "ts-jest": "^26.5.6", 27 | "ts-loader": "^9.1.2", 28 | "typescript": "^4.2.4", 29 | "webpack": "^5.36.2", 30 | "webpack-cli": "^4.7.0", 31 | "webpack-merge": "^5.7.3", 32 | "xrm-mock": "^3.5.1" 33 | }, 34 | "dependencies": { 35 | "dataverse-ify": "^1.0.10", 36 | "moment": "^2.29.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /code/clientjs/src/Forms/AccountForm.ts: -------------------------------------------------------------------------------- 1 | import { AccountAttributes } from "../dataverse-gen/entities/Account"; 2 | 3 | export class AccountForm { 4 | static async onload(context: Xrm.Events.EventContext): Promise { 5 | context.getFormContext().getAttribute(AccountAttributes.WebSiteURL).addOnChange(AccountForm.onWebsiteChanged); 6 | } 7 | static onWebsiteChanged(context: Xrm.Events.EventContext): void { 8 | const formContext = context.getFormContext(); 9 | const websiteAttribute = formContext.getAttribute(AccountAttributes.WebSiteURL); 10 | const websiteRegex = /^(https?:\/\/)?([\w\d]+\.)?[\w\d]+\.\w+\/?.+$/g; 11 | let isValid = true; 12 | if (websiteAttribute && websiteAttribute.getValue()) { 13 | const match = websiteAttribute.getValue().match(websiteRegex); 14 | isValid = match != null; 15 | } 16 | 17 | websiteAttribute.controls.forEach((c) => { 18 | if (isValid) { 19 | (c as Xrm.Controls.StringControl).clearNotification(AccountAttributes.WebSiteURL); 20 | } else { 21 | (c as Xrm.Controls.StringControl).setNotification("Invalid Website Address", AccountAttributes.WebSiteURL); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code/clientjs/src/Forms/__tests__/unit.AccountForm.test.ts: -------------------------------------------------------------------------------- 1 | import { AccountForm } from "../AccountForm"; 2 | import { XrmMockGenerator } from "xrm-mock"; 3 | 4 | describe("AccountForm.onload", () => { 5 | beforeEach(() => { 6 | XrmMockGenerator.initialise(); 7 | }); 8 | 9 | it("Handles null values", () => { 10 | // Arrange 11 | const context = XrmMockGenerator.getEventContext(); 12 | const websiteMock = XrmMockGenerator.Attribute.createString("websiteurl", undefined); 13 | websiteMock.controls.itemCollection[0].setNotification = jest.fn(); 14 | websiteMock.controls.itemCollection[0].clearNotification = jest.fn(); 15 | // Act 16 | AccountForm.onload(context); 17 | websiteMock.fireOnChange(); 18 | }); 19 | 20 | it("notifies invalid website addresses", () => { 21 | const context = XrmMockGenerator.getEventContext(); 22 | const websiteMock = XrmMockGenerator.Attribute.createString("websiteurl", "foobar"); 23 | websiteMock.controls.itemCollection[0].setNotification = jest.fn(); 24 | AccountForm.onload(context); 25 | websiteMock.fireOnChange(); 26 | expect(websiteMock.controls.itemCollection[0].setNotification).toBeCalled(); 27 | }); 28 | 29 | it("clears notification on valid website address", () => { 30 | const context = XrmMockGenerator.getEventContext(); 31 | const websiteMock = XrmMockGenerator.Attribute.createString("websiteurl", "foo"); 32 | websiteMock.controls.itemCollection[0].setNotification = jest.fn(); 33 | websiteMock.controls.itemCollection[0].clearNotification = jest.fn(); 34 | AccountForm.onload(context); 35 | websiteMock.fireOnChange(); 36 | expect(websiteMock.controls.itemCollection[0].setNotification).toBeCalledWith(expect.any(String), "websiteurl"); 37 | 38 | websiteMock.value = "https://learn.develop1.net"; 39 | websiteMock.fireOnChange(); 40 | expect(websiteMock.controls.itemCollection[0].clearNotification).toBeCalledWith("websiteurl"); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /code/clientjs/src/Mocks/MockServiceClient.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | CdsServiceClient, 4 | EntityCollection, 5 | EntityReference, 6 | Guid, 7 | IEntity, 8 | WebApiExecuteRequest, 9 | } from "dataverse-ify"; 10 | 11 | export class MockServiceClient implements CdsServiceClient { 12 | create(entity: IEntity): Promise { 13 | throw new Error("Method not implemented."); 14 | } 15 | update(entity: IEntity): Promise { 16 | throw new Error("Method not implemented."); 17 | } 18 | delete(_entityName: string | IEntity, _id?: Guid): Promise { 19 | throw new Error("Method not implemented."); 20 | } 21 | retrieve(entityName: string, id: string, columnSet: boolean | string[]): Promise { 22 | throw new Error("Method not implemented."); 23 | } 24 | retrieveMultiple(fetchxml: string): Promise> { 25 | throw new Error("Method not implemented."); 26 | } 27 | associate( 28 | entityName: string, 29 | entityId: string, 30 | relationship: string, 31 | relatedEntities: Promise, 32 | ): Promise { 33 | throw new Error("Method not implemented."); 34 | } 35 | disassociate( 36 | entityName: string, 37 | entityId: string, 38 | relationship: string, 39 | relatedEntities: EntityReference[], 40 | ): Promise { 41 | throw new Error("Method not implemented."); 42 | } 43 | execute(request: WebApiExecuteRequest): Promise { 44 | throw new Error("Method not implemented."); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /code/clientjs/src/Ribbon/AccountRibbon.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { 3 | CdsServiceClient, 4 | EntityCollection, 5 | EntityReference, 6 | setMetadataCache, 7 | XrmContextCdsServiceClient, 8 | } from "dataverse-ify"; 9 | import { WinOpportunityMetadata, WinOpportunityRequest } from "../dataverse-gen/actions/WinOpportunity"; 10 | import { Opportunity } from "../dataverse-gen/entities/Opportunity"; 11 | import { opportunitycloseMetadata } from "../dataverse-gen/entities/OpportunityClose"; 12 | import { opportunity_opportunity_statuscode } from "../dataverse-gen/enums/opportunity_opportunity_statuscode"; 13 | import { metadataCache } from "../dataverse-gen/metadata"; 14 | 15 | export class AccountRibbon { 16 | static closeOpporunities2(context: Xrm.FormContext): void { 17 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 18 | Promise.resolve(AccountRibbon.closeOpportunitiesInternal(serviceClient, context.data.entity.getId())); 19 | } 20 | static async closeOpporunities(context: Xrm.FormContext): Promise { 21 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 22 | await AccountRibbon.closeOpportunitiesInternal(serviceClient, context.data.entity.getId()); 23 | } 24 | static async closeOpportunitiesInternal(serviceClient: CdsServiceClient, accountid: string): Promise { 25 | setMetadataCache(metadataCache); 26 | // Get a list of all open opportunities 27 | const openOps = await serviceClient.retrieveMultiple(` 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | `); 37 | if (openOps.entities.length == 0) { 38 | await Xrm.Navigation.openAlertDialog({ text: `There are no open opportunties!` }); 39 | return; 40 | } 41 | const result = await Xrm.Navigation.openConfirmDialog({ 42 | title: "Close All Open Opportunities?", 43 | text: `Are you sure you want to close the ${openOps.entities.length} open opportunities?`, 44 | }); 45 | if (!result.confirmed) return; 46 | 47 | try { 48 | // For each Opportunity, Close as Won 49 | const opportunityCount = await AccountRibbon.closeOpportunities(openOps, serviceClient); 50 | Xrm.Utility.closeProgressIndicator(); 51 | await Xrm.Navigation.openAlertDialog({ title: "Success", text: `${opportunityCount} Opportunities closed` }); 52 | } catch (ex) { 53 | Xrm.Utility.closeProgressIndicator(); 54 | Xrm.Navigation.openErrorDialog({ 55 | details: ex, 56 | message: `Could not close opportunites:\n${ex.message}\n`, 57 | }); 58 | } 59 | } 60 | 61 | private static async closeOpportunities(openOps: EntityCollection, serviceClient: CdsServiceClient) { 62 | let opportunityCount = 0; 63 | for (const openOpportunity of openOps.entities) { 64 | opportunityCount++; 65 | Xrm.Utility.showProgressIndicator( 66 | `Closing Opportunity ${opportunityCount} of ${openOps.entities.length} - '${openOpportunity.name}', Please Wait...`, 67 | ); 68 | const winRequest = { 69 | logicalName: WinOpportunityMetadata.operationName, 70 | Status: opportunity_opportunity_statuscode.Won, 71 | OpportunityClose: { 72 | logicalName: opportunitycloseMetadata.logicalName, 73 | subject: "Opportunity Won", 74 | opportunityid: new EntityReference(openOpportunity.logicalName, openOpportunity.opportunityid), 75 | }, 76 | } as WinOpportunityRequest; 77 | await serviceClient.execute(winRequest); 78 | } 79 | return opportunityCount; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /code/clientjs/src/Ribbon/__tests__/integration.AccountRibbon.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { XrmMockGenerator } from "xrm-mock"; 3 | import { SetupGlobalContext } from "dataverse-ify/lib/webapi"; 4 | import { Opportunity, OpportunityAttributes, opportunityMetadata } from "../../dataverse-gen/entities/Opportunity"; 5 | import { Account, accountMetadata } from "../../dataverse-gen/entities/Account"; 6 | import { Entity, setMetadataCache, XrmContextCdsServiceClient } from "dataverse-ify"; 7 | import { AccountRibbon } from "../AccountRibbon"; 8 | import { opportunity_opportunity_statecode } from "../../dataverse-gen/enums/opportunity_opportunity_statecode"; 9 | import { metadataCache } from "../../dataverse-gen/metadata"; 10 | describe("AccountRibbon", () => { 11 | beforeEach(async () => { 12 | XrmMockGenerator.initialise(); 13 | await SetupGlobalContext(); 14 | setMetadataCache(metadataCache); 15 | Xrm.Utility.showProgressIndicator = jest.fn(); 16 | Xrm.Utility.closeProgressIndicator = jest.fn(); 17 | Xrm.Navigation.openAlertDialog = jest.fn(); 18 | Xrm.Navigation.openErrorDialog = jest.fn().mockImplementation((ex) => console.debug(JSON.stringify(ex))); 19 | Xrm.Navigation.openConfirmDialog = jest.fn().mockReturnValue({ confirmed: true } as Xrm.Navigation.ConfirmResult); 20 | }); 21 | 22 | it("closes open opportunities", async () => { 23 | // Arrange 24 | const serviceClient = new XrmContextCdsServiceClient(Xrm.WebApi); 25 | 26 | const opportunity1 = { 27 | logicalName: opportunityMetadata.logicalName, 28 | name: "Opportunity Integration Test", 29 | } as Opportunity; 30 | 31 | const account1 = { 32 | logicalName: accountMetadata.logicalName, 33 | name: "Account Integration Test", 34 | } as Account; 35 | 36 | // Act 37 | try { 38 | account1.id = await serviceClient.create(account1); 39 | opportunity1.customerid = Entity.toEntityReference(account1); 40 | opportunity1.id = await serviceClient.create(opportunity1); 41 | 42 | await AccountRibbon.closeOpportunitiesInternal(serviceClient, account1.id); 43 | 44 | // Assert 45 | // Check that the opportunity is closed 46 | const closedOp = await serviceClient.retrieve(opportunityMetadata.logicalName, opportunity1.id, [ 47 | OpportunityAttributes.StateCode, 48 | OpportunityAttributes.StatusCode, 49 | ]); 50 | expect(closedOp.statecode).toBe(opportunity_opportunity_statecode.Won); 51 | } catch (ex) { 52 | fail(ex); 53 | } finally { 54 | // Tidy up 55 | if (opportunity1.id) await serviceClient.delete(opportunity1); 56 | if (account1.id) await serviceClient.delete(account1); 57 | } 58 | }, 100000); 59 | }); 60 | -------------------------------------------------------------------------------- /code/clientjs/src/Ribbon/__tests__/unit.AccountRibbon.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { EntityCollection, EntityReference } from "dataverse-ify"; 3 | import { XrmMockGenerator } from "xrm-mock"; 4 | import { WinOpportunityRequest } from "../../dataverse-gen/actions/WinOpportunity"; 5 | import { Opportunity, opportunityMetadata } from "../../dataverse-gen/entities/Opportunity"; 6 | import { OpportunityClose, opportunitycloseMetadata } from "../../dataverse-gen/entities/OpportunityClose"; 7 | import { opportunity_opportunity_statuscode } from "../../dataverse-gen/enums/opportunity_opportunity_statuscode"; 8 | import { MockServiceClient } from "../../Mocks/MockServiceClient"; 9 | import { AccountRibbon } from "../AccountRibbon"; 10 | 11 | describe("AccountRibbon", () => { 12 | beforeEach(() => { 13 | XrmMockGenerator.initialise(); 14 | Xrm.Utility.showProgressIndicator = jest.fn(); 15 | Xrm.Utility.closeProgressIndicator = jest.fn(); 16 | Xrm.Navigation.openAlertDialog = jest.fn(); 17 | Xrm.Navigation.openErrorDialog = jest.fn().mockImplementation((ex) => console.debug(JSON.stringify(ex))); 18 | }); 19 | 20 | it("closes open opportunities", async () => { 21 | // Arrange 22 | const mockServiceClient = new MockServiceClient(); 23 | 24 | mockServiceClient.retrieveMultiple = jest.fn().mockImplementation(() => { 25 | return new EntityCollection([ 26 | { 27 | logicalName: opportunityMetadata.logicalName, 28 | opportunityid: "111", 29 | name: "AAA", 30 | } as Opportunity, 31 | ]); 32 | }); 33 | mockServiceClient.execute = jest.fn(); 34 | Xrm.Navigation.openConfirmDialog = jest.fn().mockReturnValue({ confirmed: true } as Xrm.Navigation.ConfirmResult); 35 | 36 | // Act 37 | await AccountRibbon.closeOpportunitiesInternal(mockServiceClient, "222"); 38 | 39 | // Assert 40 | expect(Xrm.Navigation.openErrorDialog).toHaveBeenCalledTimes(0); 41 | expect(mockServiceClient.execute).toHaveBeenCalledWith( 42 | expect.objectContaining({ 43 | Status: opportunity_opportunity_statuscode.Won, 44 | OpportunityClose: expect.objectContaining({ 45 | logicalName: opportunitycloseMetadata.logicalName, 46 | opportunityid: new EntityReference(opportunityMetadata.logicalName, "111"), 47 | } as OpportunityClose), 48 | } as WinOpportunityRequest), 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/actions/WinOpportunity.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import { WebApiExecuteRequest } from "dataverse-ify"; 3 | import { StructuralProperty } from "dataverse-ify"; 4 | import { OperationType } from "dataverse-ify"; 5 | 6 | // Action WinOpportunity 7 | export const WinOpportunityMetadata = { 8 | parameterTypes: { 9 | "OpportunityClose": { 10 | typeName: "mscrm.opportunityclose", 11 | structuralProperty: StructuralProperty.EntityType 12 | }, 13 | "Caller": { 14 | typeName: "Edm.String", 15 | structuralProperty: StructuralProperty.PrimitiveType 16 | }, 17 | "Status": { 18 | typeName: "Edm.Int32", 19 | structuralProperty: StructuralProperty.PrimitiveType 20 | }, 21 | 22 | }, 23 | operationType: OperationType.Action, 24 | operationName: "WinOpportunity" 25 | }; 26 | 27 | export interface WinOpportunityRequest extends WebApiExecuteRequest { 28 | OpportunityClose?: import("dataverse-ify").EntityReference | import("../entities/OpportunityClose").OpportunityClose; 29 | Caller?: string; 30 | Status?: number; 31 | } -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/entities/ActivityParty.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import { IEntity } from "dataverse-ify"; 3 | // Entity ActivityParty 4 | export const activitypartyMetadata = { 5 | typeName: "mscrm.activityparty", 6 | logicalName: "activityparty", 7 | collectionName: "activityparties", 8 | primaryIdAttribute: "activitypartyid", 9 | attributeTypes: { 10 | // Numeric Types 11 | addressusedemailcolumnnumber: "Integer", 12 | effort: "Double", 13 | versionnumber: "BigInt", 14 | // Optionsets 15 | instancetypecode: "Optionset", 16 | participationtypemask: "Optionset", 17 | // Date Formats 18 | scheduledend: "DateOnly:UserLocal", 19 | scheduledstart: "DateOnly:UserLocal", 20 | }, 21 | navigation: { 22 | resourcespecid: ["mscrm.resourcespec"], 23 | activityid: ["activitypointer"], 24 | partyid: ["account","bulkoperation","campaign","campaignactivity","contact","contract","entitlement","equipment","incident","invoice","knowledgearticle","lead","opportunity","queue","quote","salesorder","systemuser"], 25 | }, 26 | }; 27 | 28 | // Attribute constants 29 | export const enum ActivityPartyAttributes { 30 | ActivityId = "activityid", 31 | ActivityPartyId = "activitypartyid", 32 | AddressUsed = "addressused", 33 | AddressUsedEmailColumnNumber = "addressusedemailcolumnnumber", 34 | DoNotEmail = "donotemail", 35 | DoNotFax = "donotfax", 36 | DoNotPhone = "donotphone", 37 | DoNotPostalMail = "donotpostalmail", 38 | Effort = "effort", 39 | ExchangeEntryId = "exchangeentryid", 40 | InstanceTypeCode = "instancetypecode", 41 | IsPartyDeleted = "ispartydeleted", 42 | OwnerId = "ownerid", 43 | OwnerIdType = "owneridtype", 44 | OwningBusinessUnit = "owningbusinessunit", 45 | OwningUser = "owninguser", 46 | ParticipationTypeMask = "participationtypemask", 47 | PartyId = "partyid", 48 | PartyIdName = "partyidname", 49 | PartyObjectTypeCode = "partyobjecttypecode", 50 | ResourceSpecId = "resourcespecid", 51 | ResourceSpecIdName = "resourcespecidname", 52 | ScheduledEnd = "scheduledend", 53 | ScheduledStart = "scheduledstart", 54 | VersionNumber = "versionnumber", 55 | } 56 | 57 | // Early Bound Interface 58 | export interface ActivityParty extends IEntity { 59 | // Activity LookupType Unique identifier of the activity associated with the activity party. (A "party" is any person who is associated with an activity.) 60 | activityid?: import("dataverse-ify").EntityReference | null; 61 | // Activity Party UniqueidentifierType Unique identifier of the activity party. 62 | activitypartyid?: import("dataverse-ify").Guid | null; 63 | // Address StringType Email address to which an email is delivered, and which is associated with the target entity. 64 | addressused?: string | null; 65 | // Email column number of party IntegerType Email address column number from associated party. 66 | addressusedemailcolumnnumber?: number | null; 67 | // Do not allow Emails BooleanType Information about whether to allow sending email to the activity party. 68 | donotemail?: boolean | null; 69 | // Do not allow Faxes BooleanType Information about whether to allow sending faxes to the activity party. 70 | donotfax?: boolean | null; 71 | // Do not allow Phone Calls BooleanType Information about whether to allow phone calls to the lead. 72 | donotphone?: boolean | null; 73 | // Do not allow Postal Mails BooleanType Information about whether to allow sending postal mail to the lead. 74 | donotpostalmail?: boolean | null; 75 | // Effort DoubleType Amount of effort used by the resource in a service appointment activity. 76 | effort?: number | null; 77 | // Exchange Entry StringType For internal use only. 78 | exchangeentryid?: string | null; 79 | // Appointment Type activityparty_activityparty_instancetypecode Type of instance of a recurring series. 80 | instancetypecode?: import("../enums/activityparty_activityparty_instancetypecode").activityparty_activityparty_instancetypecode | null; 81 | // Is Party Deleted BooleanType Information about whether the underlying entity record is deleted. 82 | ispartydeleted?: boolean | null; 83 | // Owner [Required] OwnerType Unique identifier of the user or team who owns the activity_party. 84 | ownerid?: import("dataverse-ify").EntityReference; 85 | // EntityNameType 86 | owneridtype?: string | null; 87 | // UniqueidentifierType 88 | owningbusinessunit?: import("dataverse-ify").Guid | null; 89 | // UniqueidentifierType 90 | owninguser?: import("dataverse-ify").Guid | null; 91 | // Participation Type activityparty_activityparty_participationtypemask Role of the person in the activity, such as sender, to, cc, bcc, required, optional, organizer, regarding, or owner. 92 | participationtypemask?: import("../enums/activityparty_activityparty_participationtypemask").activityparty_activityparty_participationtypemask | null; 93 | // Party LookupType Unique identifier of the party associated with the activity. 94 | partyid?: import("dataverse-ify").EntityReference | null; 95 | // StringType 96 | partyidname?: string | null; 97 | // EntityNameType 98 | partyobjecttypecode?: string | null; 99 | // Resource Specification LookupType Unique identifier of the resource specification for the activity party. 100 | resourcespecid?: import("dataverse-ify").EntityReference | null; 101 | // StringType 102 | resourcespecidname?: string | null; 103 | // Scheduled End DateTimeType Scheduled end time of the activity. DateOnly:UserLocal 104 | scheduledend?: Date | null; 105 | // Scheduled Start DateTimeType Scheduled start time of the activity. DateOnly:UserLocal 106 | scheduledstart?: Date | null; 107 | // BigIntType 108 | versionnumber?: number | null; 109 | } 110 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/entities/OpportunityClose.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import { IEntity } from "dataverse-ify"; 3 | // Entity OpportunityClose 4 | export const opportunitycloseMetadata = { 5 | typeName: "mscrm.opportunityclose", 6 | logicalName: "opportunityclose", 7 | collectionName: "opportunitycloses", 8 | primaryIdAttribute: "activityid", 9 | attributeTypes: { 10 | // Numeric Types 11 | actualdurationminutes: "Integer", 12 | actualrevenue: "Money", 13 | actualrevenue_base: "Money", 14 | exchangerate: "Decimal", 15 | importsequencenumber: "Integer", 16 | onholdtime: "Integer", 17 | scheduleddurationminutes: "Integer", 18 | timezoneruleversionnumber: "Integer", 19 | utcconversiontimezonecode: "Integer", 20 | versionnumber: "BigInt", 21 | // Optionsets 22 | community: "Optionset", 23 | deliveryprioritycode: "Optionset", 24 | instancetypecode: "Optionset", 25 | opportunitystatecode: "Optionset", 26 | opportunitystatuscode: "Optionset", 27 | prioritycode: "Optionset", 28 | statecode: "Optionset", 29 | statuscode: "Optionset", 30 | // Date Formats 31 | actualend: "DateOnly:UserLocal", 32 | actualstart: "DateOnly:UserLocal", 33 | createdon: "DateAndTime:UserLocal", 34 | deliverylastattemptedon: "DateAndTime:UserLocal", 35 | lastonholdtime: "DateAndTime:UserLocal", 36 | modifiedon: "DateAndTime:UserLocal", 37 | overriddencreatedon: "DateOnly:UserLocal", 38 | postponeactivityprocessinguntil: "DateAndTime:UserLocal", 39 | scheduledend: "DateOnly:UserLocal", 40 | scheduledstart: "DateOnly:UserLocal", 41 | senton: "DateAndTime:UserLocal", 42 | sortdate: "DateAndTime:UserLocal", 43 | }, 44 | navigation: { 45 | sla_activitypointer_sla_opportunityclose: ["mscrm.sla"], 46 | ownerid_opportunityclose: ["mscrm.principal"], 47 | opportunityid: ["mscrm.opportunity"], 48 | competitorid: ["mscrm.competitor"], 49 | activityid_activitypointer: ["mscrm.activitypointer"], 50 | createdby: ["systemuser"], 51 | createdonbehalfby: ["systemuser"], 52 | modifiedby: ["systemuser"], 53 | modifiedonbehalfby: ["systemuser"], 54 | owningbusinessunit: ["businessunit"], 55 | owningteam: ["team"], 56 | owninguser: ["systemuser"], 57 | regardingobjectid: ["account","bookableresourcebooking","bookableresourcebookingheader","bulkoperation","campaign","campaignactivity","entitlement","entitlementtemplate","incident","interactionforemail","knowledgearticle","knowledgebaserecord","lead","msdyn_customerasset","msdyn_playbookinstance","opportunity","site"], 58 | sendermailboxid: ["mailbox"], 59 | serviceid: ["service"], 60 | slainvokedid: ["sla"], 61 | transactioncurrencyid: ["transactioncurrency"], 62 | }, 63 | }; 64 | 65 | // Attribute constants 66 | export const enum OpportunityCloseAttributes { 67 | ActivityAdditionalParams = "activityadditionalparams", 68 | ActivityId = "activityid", 69 | ActivityTypeCode = "activitytypecode", 70 | ActualDurationMinutes = "actualdurationminutes", 71 | ActualEnd = "actualend", 72 | ActualRevenue = "actualrevenue", 73 | ActualRevenue_Base = "actualrevenue_base", 74 | ActualStart = "actualstart", 75 | BCC = "bcc", 76 | Category = "category", 77 | CC = "cc", 78 | Community = "community", 79 | CompetitorId = "competitorid", 80 | CompetitorIdName = "competitoridname", 81 | CompetitorIdYomiName = "competitoridyominame", 82 | CreatedBy = "createdby", 83 | CreatedByExternalParty = "createdbyexternalparty", 84 | CreatedByExternalPartyName = "createdbyexternalpartyname", 85 | CreatedByExternalPartyYomiName = "createdbyexternalpartyyominame", 86 | CreatedByName = "createdbyname", 87 | CreatedByYomiName = "createdbyyominame", 88 | CreatedOn = "createdon", 89 | CreatedOnBehalfBy = "createdonbehalfby", 90 | CreatedOnBehalfByName = "createdonbehalfbyname", 91 | CreatedOnBehalfByYomiName = "createdonbehalfbyyominame", 92 | Customers = "customers", 93 | DeliveryLastAttemptedOn = "deliverylastattemptedon", 94 | DeliveryPriorityCode = "deliveryprioritycode", 95 | Description = "description", 96 | ExchangeItemId = "exchangeitemid", 97 | ExchangeRate = "exchangerate", 98 | ExchangeWebLink = "exchangeweblink", 99 | From = "from", 100 | ImportSequenceNumber = "importsequencenumber", 101 | InstanceTypeCode = "instancetypecode", 102 | IsBilled = "isbilled", 103 | IsMapiPrivate = "ismapiprivate", 104 | IsRegularActivity = "isregularactivity", 105 | IsWorkflowCreated = "isworkflowcreated", 106 | LastOnHoldTime = "lastonholdtime", 107 | LeftVoiceMail = "leftvoicemail", 108 | ModifiedBy = "modifiedby", 109 | ModifiedByExternalParty = "modifiedbyexternalparty", 110 | ModifiedByExternalPartyName = "modifiedbyexternalpartyname", 111 | ModifiedByExternalPartyYomiName = "modifiedbyexternalpartyyominame", 112 | ModifiedByName = "modifiedbyname", 113 | ModifiedByYomiName = "modifiedbyyominame", 114 | ModifiedOn = "modifiedon", 115 | ModifiedOnBehalfBy = "modifiedonbehalfby", 116 | ModifiedOnBehalfByName = "modifiedonbehalfbyname", 117 | ModifiedOnBehalfByYomiName = "modifiedonbehalfbyyominame", 118 | OnHoldTime = "onholdtime", 119 | OpportunityId = "opportunityid", 120 | OpportunityIdName = "opportunityidname", 121 | OpportunityIdType = "opportunityidtype", 122 | OpportunityStateCode = "opportunitystatecode", 123 | OpportunityStatusCode = "opportunitystatuscode", 124 | OptionalAttendees = "optionalattendees", 125 | Organizer = "organizer", 126 | OverriddenCreatedOn = "overriddencreatedon", 127 | OwnerId = "ownerid", 128 | OwnerIdName = "owneridname", 129 | OwnerIdType = "owneridtype", 130 | OwnerIdYomiName = "owneridyominame", 131 | OwningBusinessUnit = "owningbusinessunit", 132 | OwningTeam = "owningteam", 133 | OwningUser = "owninguser", 134 | Partners = "partners", 135 | PostponeActivityProcessingUntil = "postponeactivityprocessinguntil", 136 | PriorityCode = "prioritycode", 137 | ProcessId = "processid", 138 | RegardingObjectId = "regardingobjectid", 139 | RegardingObjectIdName = "regardingobjectidname", 140 | RegardingObjectIdYomiName = "regardingobjectidyominame", 141 | RegardingObjectTypeCode = "regardingobjecttypecode", 142 | RequiredAttendees = "requiredattendees", 143 | Resources = "resources", 144 | ScheduledDurationMinutes = "scheduleddurationminutes", 145 | ScheduledEnd = "scheduledend", 146 | ScheduledStart = "scheduledstart", 147 | SenderMailboxId = "sendermailboxid", 148 | SenderMailboxIdName = "sendermailboxidname", 149 | SentOn = "senton", 150 | SeriesId = "seriesid", 151 | ServiceId = "serviceid", 152 | ServiceIdName = "serviceidname", 153 | SLAId = "slaid", 154 | SLAInvokedId = "slainvokedid", 155 | SLAInvokedIdName = "slainvokedidname", 156 | SLAName = "slaname", 157 | SortDate = "sortdate", 158 | StageId = "stageid", 159 | StateCode = "statecode", 160 | StatusCode = "statuscode", 161 | Subcategory = "subcategory", 162 | Subject = "subject", 163 | TimeZoneRuleVersionNumber = "timezoneruleversionnumber", 164 | To = "to", 165 | TransactionCurrencyId = "transactioncurrencyid", 166 | TransactionCurrencyIdName = "transactioncurrencyidname", 167 | TraversedPath = "traversedpath", 168 | UTCConversionTimeZoneCode = "utcconversiontimezonecode", 169 | VersionNumber = "versionnumber", 170 | } 171 | 172 | // Early Bound Interface 173 | export interface OpportunityClose extends IEntity { 174 | // Activity Additional Parameters MemoType Additional information provided by the external application as JSON. For internal use only. 175 | activityadditionalparams?: string | null; 176 | // Opportunity Close UniqueidentifierType Unique identifier of the opportunity close activity. 177 | activityid?: import("dataverse-ify").Guid | null; 178 | // Activity Type EntityNameType Type of activity. 179 | activitytypecode?: string | null; 180 | // Actual Duration IntegerType Actual duration of the opportunity close activity in minutes. 181 | actualdurationminutes?: number | null; 182 | // Closed On DateTimeType Actual end time of the opportunity close activity. DateOnly:UserLocal 183 | actualend?: Date | null; 184 | // Actual Revenue MoneyType Actual revenue generated for the opportunity. 185 | actualrevenue?: number | null; 186 | // Actual Revenue (Base) MoneyType Value of the Actual Revenue in base currency. 187 | actualrevenue_base?: number | null; 188 | // Actual Start DateTimeType Actual start time of the opportunity close activity. DateOnly:UserLocal 189 | actualstart?: Date | null; 190 | // BCC PartyListType Blind Carbon-copy (bcc) recipients of the activity. 191 | bcc?: import("dataverse-ify").ActivityParty[] | null; 192 | // Category StringType Category of the opportunity close activity. 193 | category?: string | null; 194 | // CC PartyListType Carbon-copy (cc) recipients of the activity. 195 | cc?: import("dataverse-ify").ActivityParty[] | null; 196 | // Social Channel socialprofile_community Shows how contact about the social activity originated, such as from Twitter or Facebook. This field is read-only. 197 | community?: import("../enums/socialprofile_community").socialprofile_community | null; 198 | // Competitor LookupType Unique identifier of the competitor with which the opportunity close activity is associated. 199 | competitorid?: import("dataverse-ify").EntityReference | null; 200 | // StringType 201 | competitoridname?: string | null; 202 | // StringType 203 | competitoridyominame?: string | null; 204 | // Created By LookupType Unique identifier of the user who created the opportunity close activity. 205 | createdby?: import("dataverse-ify").EntityReference | null; 206 | // Created By (External Party) LookupType Shows the external party who created the record. 207 | createdbyexternalparty?: import("dataverse-ify").EntityReference | null; 208 | // StringType 209 | createdbyexternalpartyname?: string | null; 210 | // StringType 211 | createdbyexternalpartyyominame?: string | null; 212 | // StringType 213 | createdbyname?: string | null; 214 | // StringType 215 | createdbyyominame?: string | null; 216 | // Created On DateTimeType Date and time when the opportunity close activity was created. DateAndTime:UserLocal 217 | createdon?: Date | null; 218 | // Created By (Delegate) LookupType Unique identifier of the delegate user who created the opportunityclose. 219 | createdonbehalfby?: import("dataverse-ify").EntityReference | null; 220 | // StringType 221 | createdonbehalfbyname?: string | null; 222 | // StringType 223 | createdonbehalfbyyominame?: string | null; 224 | // Customers PartyListType Customer with which the activity is associated. 225 | customers?: import("dataverse-ify").ActivityParty[] | null; 226 | // Date Delivery Last Attempted DateTimeType Date and time when the delivery of the activity was last attempted. DateAndTime:UserLocal 227 | deliverylastattemptedon?: Date | null; 228 | // Delivery Priority activitypointer_deliveryprioritycode Priority of delivery of the activity to the email server. 229 | deliveryprioritycode?: import("../enums/activitypointer_deliveryprioritycode").activitypointer_deliveryprioritycode | null; 230 | // Description MemoType Activity that is created automatically when an opportunity is closed, containing information such as the description of the closing and actual revenue. 231 | description?: string | null; 232 | // Exchange Item ID StringType The message id of activity which is returned from Exchange Server. 233 | exchangeitemid?: string | null; 234 | // Exchange Rate DecimalType Shows the conversion rate of the record's currency. The exchange rate is used to convert all money fields in the record from the local currency to the system's default currency. 235 | exchangerate?: number | null; 236 | // Exchange WebLink StringType Shows the web link of Activity of type email. 237 | exchangeweblink?: string | null; 238 | // From PartyListType Person who the activity is from. 239 | from?: import("dataverse-ify").ActivityParty[] | null; 240 | // Import Sequence Number IntegerType Sequence number of the import that created this record. 241 | importsequencenumber?: number | null; 242 | // Recurring Instance Type opportunityclose__opportunityclose_instancetypecode Type of instance of a recurring series. 243 | instancetypecode?: import("../enums/opportunityclose__opportunityclose_instancetypecode").opportunityclose__opportunityclose_instancetypecode | null; 244 | // Is Billed BooleanType Information about whether the opportunity close activity was billed as part of resolving a case. 245 | isbilled?: boolean | null; 246 | // Is Private BooleanType For internal use only. 247 | ismapiprivate?: boolean | null; 248 | // Is Regular Activity BooleanType Information regarding whether the activity is a regular activity type or event type. 249 | isregularactivity?: boolean | null; 250 | // Is Workflow Created BooleanType Information that specifies if the opportunity close activity was created from a workflow rule. 251 | isworkflowcreated?: boolean | null; 252 | // Last On Hold Time DateTimeType Contains the date and time stamp of the last on hold time. DateAndTime:UserLocal 253 | lastonholdtime?: Date | null; 254 | // Left Voice Mail BooleanType Left the voice mail 255 | leftvoicemail?: boolean | null; 256 | // Modified By LookupType Unique identifier of the user who last modified the opportunity close activity. 257 | modifiedby?: import("dataverse-ify").EntityReference | null; 258 | // Modified By (External Party) LookupType Shows the external party who modified the record. 259 | modifiedbyexternalparty?: import("dataverse-ify").EntityReference | null; 260 | // StringType 261 | modifiedbyexternalpartyname?: string | null; 262 | // StringType 263 | modifiedbyexternalpartyyominame?: string | null; 264 | // StringType 265 | modifiedbyname?: string | null; 266 | // StringType 267 | modifiedbyyominame?: string | null; 268 | // Modified On DateTimeType Date and time when the opportunity close activity was last modified. DateAndTime:UserLocal 269 | modifiedon?: Date | null; 270 | // Modified By (Delegate) LookupType Unique identifier of the delegate user who last modified the opportunityclose. 271 | modifiedonbehalfby?: import("dataverse-ify").EntityReference | null; 272 | // StringType 273 | modifiedonbehalfbyname?: string | null; 274 | // StringType 275 | modifiedonbehalfbyyominame?: string | null; 276 | // On Hold Time (Minutes) IntegerType Shows how long, in minutes, that the record was on hold. 277 | onholdtime?: number | null; 278 | // Opportunity [Required] LookupType Unique identifier of the opportunity closed. 279 | opportunityid?: import("dataverse-ify").EntityReference; 280 | // StringType 281 | opportunityidname?: string | null; 282 | // EntityNameType 283 | opportunityidtype?: string | null; 284 | // Status opportunityclose_opportunityclose_opportunity_statecode Status of the opportunity. 285 | opportunitystatecode?: import("../enums/opportunityclose_opportunityclose_opportunity_statecode").opportunityclose_opportunityclose_opportunity_statecode | null; 286 | // Status Reason opportunityclose_OpportunityClose_opportunity_statuscode Status reason of the opportunity. 287 | opportunitystatuscode?: import("../enums/opportunityclose_OpportunityClose_opportunity_statuscode").opportunityclose_OpportunityClose_opportunity_statuscode | null; 288 | // Optional Attendees PartyListType List of optional attendees for the activity. 289 | optionalattendees?: import("dataverse-ify").ActivityParty[] | null; 290 | // Organizer PartyListType Person who organized the activity. 291 | organizer?: import("dataverse-ify").ActivityParty[] | null; 292 | // Record Created On DateTimeType Date and time that the record was migrated. DateOnly:UserLocal 293 | overriddencreatedon?: Date | null; 294 | // Owner OwnerType Unique identifier of the user or team who owns the activity. 295 | ownerid?: import("dataverse-ify").EntityReference | null; 296 | // StringType 297 | owneridname?: string | null; 298 | // EntityNameType 299 | owneridtype?: string | null; 300 | // StringType 301 | owneridyominame?: string | null; 302 | // Owning Business Unit LookupType Unique identifier of the business unit that owns the activity. 303 | owningbusinessunit?: import("dataverse-ify").EntityReference | null; 304 | // Owning Team LookupType Unique identifier of the team that owns the activity. 305 | owningteam?: import("dataverse-ify").EntityReference | null; 306 | // Owning User LookupType Unique identifier of the user that owns the activity. 307 | owninguser?: import("dataverse-ify").EntityReference | null; 308 | // Outsource Vendors PartyListType Outsource vendor with which activity is associated. 309 | partners?: import("dataverse-ify").ActivityParty[] | null; 310 | // Delay activity processing until DateTimeType For internal use only. DateAndTime:UserLocal 311 | postponeactivityprocessinguntil?: Date | null; 312 | // Priority opportunityclose__opportunityclose_prioritycode Priority of the activity. 313 | prioritycode?: import("../enums/opportunityclose__opportunityclose_prioritycode").opportunityclose__opportunityclose_prioritycode | null; 314 | // Process UniqueidentifierType Unique identifier of the Process. 315 | processid?: import("dataverse-ify").Guid | null; 316 | // Regarding LookupType Unique identifier of the object with which the activity is associated. 317 | regardingobjectid?: import("dataverse-ify").EntityReference | null; 318 | // StringType 319 | regardingobjectidname?: string | null; 320 | // StringType 321 | regardingobjectidyominame?: string | null; 322 | // EntityNameType 323 | regardingobjecttypecode?: string | null; 324 | // Required Attendees PartyListType List of required attendees for the activity. 325 | requiredattendees?: import("dataverse-ify").ActivityParty[] | null; 326 | // Resources PartyListType Users or facility/equipment that are required for the activity. 327 | resources?: import("dataverse-ify").ActivityParty[] | null; 328 | // Scheduled Duration IntegerType Scheduled duration of the opportunity close activity, specified in minutes. 329 | scheduleddurationminutes?: number | null; 330 | // Scheduled End DateTimeType Scheduled end time of the opportunity close activity. DateOnly:UserLocal 331 | scheduledend?: Date | null; 332 | // Scheduled Start DateTimeType Scheduled start time of the opportunity close activity. DateOnly:UserLocal 333 | scheduledstart?: Date | null; 334 | // Sender's Mailbox LookupType Unique identifier of the mailbox associated with the sender of the email message. 335 | sendermailboxid?: import("dataverse-ify").EntityReference | null; 336 | // StringType 337 | sendermailboxidname?: string | null; 338 | // Date Sent DateTimeType Date and time when the activity was sent. DateAndTime:UserLocal 339 | senton?: Date | null; 340 | // Series Id UniqueidentifierType Uniqueidentifier specifying the id of recurring series of an instance. 341 | seriesid?: import("dataverse-ify").Guid | null; 342 | // Service LookupType Unique identifier of the service with which the opportunity close activity is associated. 343 | serviceid?: import("dataverse-ify").EntityReference | null; 344 | // StringType 345 | serviceidname?: string | null; 346 | // SLA LookupType Choose the service level agreement (SLA) that you want to apply to the case record. 347 | slaid?: import("dataverse-ify").EntityReference | null; 348 | // Last SLA applied LookupType Last SLA that was applied to this case. This field is for internal use only. 349 | slainvokedid?: import("dataverse-ify").EntityReference | null; 350 | // StringType 351 | slainvokedidname?: string | null; 352 | // StringType 353 | slaname?: string | null; 354 | // Sort Date DateTimeType Shows the date and time by which the activities are sorted. DateAndTime:UserLocal 355 | sortdate?: Date | null; 356 | // (Deprecated) Process Stage UniqueidentifierType Unique identifier of the Stage. 357 | stageid?: import("dataverse-ify").Guid | null; 358 | // Status opportunityclose_opportunityclose_statecode Shows whether the opportunity close activity is open, completed, or canceled. By default, opportunity close activities are completed unless the opportunity is reactivated, which updates them to canceled. 359 | statecode?: import("../enums/opportunityclose_opportunityclose_statecode").opportunityclose_opportunityclose_statecode | null; 360 | // Status Reason opportunityclose_opportunityclose_statuscode Reason for the status of the opportunity close activity. 361 | statuscode?: import("../enums/opportunityclose_opportunityclose_statuscode").opportunityclose_opportunityclose_statuscode | null; 362 | // Sub-Category StringType Subcategory of the opportunity close activity. 363 | subcategory?: string | null; 364 | // Subject StringType Subject associated with the opportunity close activity. 365 | subject?: string | null; 366 | // Time Zone Rule Version Number IntegerType For internal use only. 367 | timezoneruleversionnumber?: number | null; 368 | // To PartyListType Person who is the receiver of the activity. 369 | to?: import("dataverse-ify").ActivityParty[] | null; 370 | // Currency LookupType Choose the local currency for the record to make sure budgets are reported in the correct currency. 371 | transactioncurrencyid?: import("dataverse-ify").EntityReference | null; 372 | // StringType 373 | transactioncurrencyidname?: string | null; 374 | // (Deprecated) Traversed Path StringType For internal use only. 375 | traversedpath?: string | null; 376 | // UTC Conversion Time Zone Code IntegerType Time zone code that was in use when the record was created. 377 | utcconversiontimezonecode?: number | null; 378 | // Version Number BigIntType Version number of the activity. 379 | versionnumber?: number | null; 380 | } 381 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_accountcategorycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_accountcategorycode 3 | export const enum account_account_accountcategorycode { 4 | PreferredCustomer = 1, 5 | Standard = 2, 6 | } 7 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_accountclassificationcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_accountclassificationcode 3 | export const enum account_account_accountclassificationcode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_accountratingcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_accountratingcode 3 | export const enum account_account_accountratingcode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_address1_addresstypecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_address1_addresstypecode 3 | export const enum account_account_address1_addresstypecode { 4 | BillTo = 1, 5 | ShipTo = 2, 6 | Primary = 3, 7 | Other = 4, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_address1_freighttermscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_address1_freighttermscode 3 | export const enum account_account_address1_freighttermscode { 4 | FOB = 1, 5 | NoCharge = 2, 6 | } 7 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_address1_shippingmethodcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_address1_shippingmethodcode 3 | export const enum account_account_address1_shippingmethodcode { 4 | Airborne = 1, 5 | DHL = 2, 6 | FedEx = 3, 7 | UPS = 4, 8 | PostalMail = 5, 9 | FullLoad = 6, 10 | WillCall = 7, 11 | } 12 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_address2_addresstypecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_address2_addresstypecode 3 | export const enum account_account_address2_addresstypecode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_address2_freighttermscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_address2_freighttermscode 3 | export const enum account_account_address2_freighttermscode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_address2_shippingmethodcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_address2_shippingmethodcode 3 | export const enum account_account_address2_shippingmethodcode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_businesstypecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_businesstypecode 3 | export const enum account_account_businesstypecode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_customersizecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_customersizecode 3 | export const enum account_account_customersizecode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_customertypecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_customertypecode 3 | export const enum account_account_customertypecode { 4 | Competitor = 1, 5 | Consultant = 2, 6 | Customer = 3, 7 | Investor = 4, 8 | Partner = 5, 9 | Influencer = 6, 10 | Press = 7, 11 | Prospect = 8, 12 | Reseller = 9, 13 | Supplier = 10, 14 | Vendor = 11, 15 | Other = 12, 16 | } 17 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_industrycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_industrycode 3 | export const enum account_account_industrycode { 4 | Accounting = 1, 5 | AgricultureandNonpetrolNaturalResourceExtraction = 2, 6 | BroadcastingPrintingandPublishing = 3, 7 | Brokers = 4, 8 | BuildingSupplyRetail = 5, 9 | BusinessServices = 6, 10 | Consulting = 7, 11 | ConsumerServices = 8, 12 | DesignDirectionandCreativeManagement = 9, 13 | DistributorsDispatchersandProcessors = 10, 14 | DoctorsOfficesandClinics = 11, 15 | DurableManufacturing = 12, 16 | EatingandDrinkingPlaces = 13, 17 | EntertainmentRetail = 14, 18 | EquipmentRentalandLeasing = 15, 19 | Financial = 16, 20 | FoodandTobaccoProcessing = 17, 21 | InboundCapitalIntensiveProcessing = 18, 22 | InboundRepairandServices = 19, 23 | Insurance = 20, 24 | LegalServices = 21, 25 | NonDurableMerchandiseRetail = 22, 26 | OutboundConsumerService = 23, 27 | PetrochemicalExtractionandDistribution = 24, 28 | ServiceRetail = 25, 29 | SIGAffiliations = 26, 30 | SocialServices = 27, 31 | SpecialOutboundTradeContractors = 28, 32 | SpecialtyRealty = 29, 33 | Transportation = 30, 34 | UtilityCreationandDistribution = 31, 35 | VehicleRetail = 32, 36 | Wholesale = 33, 37 | } 38 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_ownershipcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_ownershipcode 3 | export const enum account_account_ownershipcode { 4 | Public = 1, 5 | Private = 2, 6 | Subsidiary = 3, 7 | Other = 4, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_paymenttermscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_paymenttermscode 3 | export const enum account_account_paymenttermscode { 4 | Net30 = 1, 5 | _210Net30 = 2, 6 | Net45 = 3, 7 | Net60 = 4, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_preferredappointmentdaycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_preferredappointmentdaycode 3 | export const enum account_account_preferredappointmentdaycode { 4 | Sunday = 0, 5 | Monday = 1, 6 | Tuesday = 2, 7 | Wednesday = 3, 8 | Thursday = 4, 9 | Friday = 5, 10 | Saturday = 6, 11 | } 12 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_preferredappointmenttimecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_preferredappointmenttimecode 3 | export const enum account_account_preferredappointmenttimecode { 4 | Morning = 1, 5 | Afternoon = 2, 6 | Evening = 3, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_preferredcontactmethodcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_preferredcontactmethodcode 3 | export const enum account_account_preferredcontactmethodcode { 4 | Any = 1, 5 | Email = 2, 6 | Phone = 3, 7 | Fax = 4, 8 | Mail = 5, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_shippingmethodcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_shippingmethodcode 3 | export const enum account_account_shippingmethodcode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_statecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_statecode 3 | export const enum account_account_statecode { 4 | Active = 0, 5 | Inactive = 1, 6 | } 7 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_statuscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_statuscode 3 | export const enum account_account_statuscode { 4 | Active = 1, 5 | Inactive = 2, 6 | } 7 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/account_account_territorycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum account_account_territorycode 3 | export const enum account_account_territorycode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/activityparty_activityparty_instancetypecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum activityparty_activityparty_instancetypecode 3 | export const enum activityparty_activityparty_instancetypecode { 4 | NotRecurring = 0, 5 | RecurringMaster = 1, 6 | RecurringInstance = 2, 7 | RecurringException = 3, 8 | RecurringFutureException = 4, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/activityparty_activityparty_participationtypemask.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum activityparty_activityparty_participationtypemask 3 | export const enum activityparty_activityparty_participationtypemask { 4 | Sender = 1, 5 | ToRecipient = 2, 6 | CCRecipient = 3, 7 | BCCRecipient = 4, 8 | Requiredattendee = 5, 9 | Optionalattendee = 6, 10 | Organizer = 7, 11 | Regarding = 8, 12 | Owner = 9, 13 | Resource = 10, 14 | Customer = 11, 15 | } 16 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/activitypointer_deliveryprioritycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum activitypointer_deliveryprioritycode 3 | export const enum activitypointer_deliveryprioritycode { 4 | Low = 0, 5 | Normal = 1, 6 | High = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/budgetstatus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum budgetstatus 3 | export const enum budgetstatus { 4 | NoCommittedBudget = 0, 5 | MayBuy = 1, 6 | CanBuy = 2, 7 | WillBuy = 3, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_correlationmethod.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_correlationmethod 3 | export const enum email_email_correlationmethod { 4 | None = 0, 5 | Skipped = 1, 6 | XHeader = 2, 7 | InReplyTo = 3, 8 | TrackingToken = 4, 9 | ConversationIndex = 5, 10 | SmartMatching = 6, 11 | CustomCorrelation = 7, 12 | } 13 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_notifications.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_notifications 3 | export const enum email_email_notifications { 4 | None = 0, 5 | ThemessagewassavedasaMicrosoftDynamics365emailrecordbutnotalltheattachmentscouldbesavedwithitAnattachmentcannotbesavedifitisblockedorifitsfiletypeisinvalid = 1, 6 | Truncatedbody = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_prioritycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_prioritycode 3 | export const enum email_email_prioritycode { 4 | Low = 0, 5 | Normal = 1, 6 | High = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_reminderstatus.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_reminderstatus 3 | export const enum email_email_reminderstatus { 4 | NotSet = 0, 5 | ReminderSet = 1, 6 | ReminderExpired = 2, 7 | ReminderInvalid = 3, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_remindertype.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_remindertype 3 | export const enum email_email_remindertype { 4 | IfIdonotreceiveareplyby = 0, 5 | Iftheemailisnotopenedby = 1, 6 | Remindmeanywaysat = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_statecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_statecode 3 | export const enum email_email_statecode { 4 | Open = 0, 5 | Completed = 1, 6 | Canceled = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/email_email_statuscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum email_email_statuscode 3 | export const enum email_email_statuscode { 4 | Draft = 1, 5 | Completed = 2, 6 | Sent = 3, 7 | Received = 4, 8 | Canceled = 5, 9 | PendingSend = 6, 10 | Sending = 7, 11 | Failed = 8, 12 | } 13 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/initialcommunication.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum initialcommunication 3 | export const enum initialcommunication { 4 | Contacted = 0, 5 | NotContacted = 1, 6 | } 7 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/msdyn_travelchargetype.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum msdyn_travelchargetype 3 | export const enum msdyn_travelchargetype { 4 | Hourly = 690970000, 5 | Mileage = 690970001, 6 | Fixed = 690970002, 7 | None = 690970003, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/need.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum need 3 | export const enum need { 4 | Musthave = 0, 5 | Shouldhave = 1, 6 | Goodtohave = 2, 7 | Noneed = 3, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_msdyn_opportunity_msdyn_forecastcategory.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_msdyn_opportunity_msdyn_forecastcategory 3 | export const enum opportunity_msdyn_opportunity_msdyn_forecastcategory { 4 | Pipeline = 100000001, 5 | Bestcase = 100000002, 6 | Committed = 100000003, 7 | Omitted = 100000004, 8 | Won = 100000005, 9 | Lost = 100000006, 10 | } 11 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_msdyn_opportunity_msdyn_ordertype.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_msdyn_opportunity_msdyn_ordertype 3 | export const enum opportunity_msdyn_opportunity_msdyn_ordertype { 4 | Workbased = 192350001, 5 | Itembased = 192350000, 6 | ServiceMaintenanceBased = 690970002, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_opportunity_opportunityratingcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_opportunity_opportunityratingcode 3 | export const enum opportunity_opportunity_opportunityratingcode { 4 | Hot = 1, 5 | Warm = 2, 6 | Cold = 3, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_opportunity_prioritycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_opportunity_prioritycode 3 | export const enum opportunity_opportunity_prioritycode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_opportunity_salesstagecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_opportunity_salesstagecode 3 | export const enum opportunity_opportunity_salesstagecode { 4 | DefaultValue = 1, 5 | } 6 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_opportunity_statecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_opportunity_statecode 3 | export const enum opportunity_opportunity_statecode { 4 | Open = 0, 5 | Won = 1, 6 | Lost = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_opportunity_statuscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_opportunity_statuscode 3 | export const enum opportunity_opportunity_statuscode { 4 | InProgress = 1, 5 | OnHold = 2, 6 | Won = 3, 7 | Canceled = 4, 8 | OutSold = 5, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_opportunity_timeline.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_opportunity_timeline 3 | export const enum opportunity_opportunity_timeline { 4 | Immediate = 0, 5 | ThisQuarter = 1, 6 | NextQuarter = 2, 7 | ThisYear = 3, 8 | Notknown = 4, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunity_salesstage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunity_salesstage 3 | export const enum opportunity_salesstage { 4 | Qualify = 0, 5 | Develop = 1, 6 | Propose = 2, 7 | Close = 3, 8 | } 9 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunityclose_OpportunityClose_opportunity_statuscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunityclose_OpportunityClose_opportunity_statuscode 3 | export const enum opportunityclose_OpportunityClose_opportunity_statuscode { 4 | InProgress = 1, 5 | OnHold = 2, 6 | Won = 3, 7 | Canceled = 4, 8 | OutSold = 5, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunityclose__opportunityclose_instancetypecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunityclose__opportunityclose_instancetypecode 3 | export const enum opportunityclose__opportunityclose_instancetypecode { 4 | NotRecurring = 0, 5 | RecurringMaster = 1, 6 | RecurringInstance = 2, 7 | RecurringException = 3, 8 | RecurringFutureException = 4, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunityclose__opportunityclose_prioritycode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunityclose__opportunityclose_prioritycode 3 | export const enum opportunityclose__opportunityclose_prioritycode { 4 | Low = 0, 5 | Normal = 1, 6 | High = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunityclose_opportunityclose_opportunity_statecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunityclose_opportunityclose_opportunity_statecode 3 | export const enum opportunityclose_opportunityclose_opportunity_statecode { 4 | Open = 0, 5 | Won = 1, 6 | Lost = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunityclose_opportunityclose_statecode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunityclose_opportunityclose_statecode 3 | export const enum opportunityclose_opportunityclose_statecode { 4 | Open = 0, 5 | Completed = 1, 6 | Canceled = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/opportunityclose_opportunityclose_statuscode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum opportunityclose_opportunityclose_statuscode 3 | export const enum opportunityclose_opportunityclose_statuscode { 4 | Open = 1, 5 | Completed = 2, 6 | Canceled = 3, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/purchaseprocess.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum purchaseprocess 3 | export const enum purchaseprocess { 4 | Individual = 0, 5 | Committee = 1, 6 | Unknown = 2, 7 | } 8 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/purchasetimeframe.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum purchasetimeframe 3 | export const enum purchasetimeframe { 4 | Immediate = 0, 5 | ThisQuarter = 1, 6 | NextQuarter = 2, 7 | ThisYear = 3, 8 | Unknown = 4, 9 | } 10 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/qooi_pricingerrorcode.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum qooi_pricingerrorcode 3 | export const enum qooi_pricingerrorcode { 4 | None = 0, 5 | DetailError = 1, 6 | MissingPriceLevel = 2, 7 | InactivePriceLevel = 3, 8 | MissingQuantity = 4, 9 | MissingUnitPrice = 5, 10 | MissingProduct = 6, 11 | InvalidProduct = 7, 12 | MissingPricingCode = 8, 13 | InvalidPricingCode = 9, 14 | MissingUOM = 10, 15 | ProductNotInPriceLevel = 11, 16 | MissingPriceLevelAmount = 12, 17 | MissingPriceLevelPercentage = 13, 18 | MissingPrice = 14, 19 | MissingCurrentCost = 15, 20 | MissingStandardCost = 16, 21 | InvalidPriceLevelAmount = 17, 22 | InvalidPriceLevelPercentage = 18, 23 | InvalidPrice = 19, 24 | InvalidCurrentCost = 20, 25 | InvalidStandardCost = 21, 26 | InvalidRoundingPolicy = 22, 27 | InvalidRoundingOption = 23, 28 | InvalidRoundingAmount = 24, 29 | PriceCalculationError = 25, 30 | InvalidDiscountType = 26, 31 | DiscountTypeInvalidState = 27, 32 | InvalidDiscount = 28, 33 | InvalidQuantity = 29, 34 | InvalidPricingPrecision = 30, 35 | MissingProductDefaultUOM = 31, 36 | MissingProductUOMSchedule = 32, 37 | InactiveDiscountType = 33, 38 | InvalidPriceLevelCurrency = 34, 39 | PriceAttributeOutOfRange = 35, 40 | BaseCurrencyAttributeOverflow = 36, 41 | BaseCurrencyAttributeUnderflow = 37, 42 | Transactioncurrencyisnotsetfortheproductpricelistitem = 38, 43 | } 44 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/qooi_skippricecalculation.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum qooi_skippricecalculation 3 | export const enum qooi_skippricecalculation { 4 | DoPriceCalcAlways = 0, 5 | SkipPriceCalcOnRetrieve = 1, 6 | } 7 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/enums/socialprofile_community.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | // Enum socialprofile_community 3 | export const enum socialprofile_community { 4 | Cortana = 5, 5 | DirectLine = 6, 6 | MicrosoftTeams = 7, 7 | DirectLineSpeech = 8, 8 | Email = 9, 9 | GroupMe = 10, 10 | Kik = 11, 11 | Telegram = 12, 12 | Skype = 13, 13 | Slack = 14, 14 | WhatsApp = 15, 15 | Line = 3, 16 | Wechat = 4, 17 | Facebook = 1, 18 | Twitter = 2, 19 | Other = 0, 20 | } 21 | -------------------------------------------------------------------------------- /code/clientjs/src/dataverse-gen/metadata.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import { accountMetadata } from "./entities/Account"; 3 | import { activitypartyMetadata } from "./entities/ActivityParty"; 4 | import { emailMetadata } from "./entities/Email"; 5 | import { opportunityMetadata } from "./entities/Opportunity"; 6 | import { opportunitycloseMetadata } from "./entities/OpportunityClose"; 7 | import { WinOpportunityMetadata } from "./actions/WinOpportunity"; 8 | 9 | export const Entities = { 10 | Account: "account", 11 | ActivityParty: "activityparty", 12 | Email: "email", 13 | Opportunity: "opportunity", 14 | OpportunityClose: "opportunityclose", 15 | }; 16 | 17 | // Setup Metadata 18 | // Usage: setMetadataCache(metadataCache); 19 | export const metadataCache = { 20 | entities: { 21 | account: accountMetadata, 22 | activityparty: activitypartyMetadata, 23 | email: emailMetadata, 24 | opportunity: opportunityMetadata, 25 | opportunityclose: opportunitycloseMetadata, 26 | }, 27 | actions: { 28 | WinOpportunity: WinOpportunityMetadata, 29 | } 30 | }; -------------------------------------------------------------------------------- /code/clientjs/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Forms/AccountForm"; 2 | export * from "./Ribbon/AccountRibbon"; 3 | -------------------------------------------------------------------------------- /code/clientjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": [ 10 | "dom", 11 | "es2015" 12 | ], /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 16 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 17 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 18 | "sourceMap": true, /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | // "outDir": "./", /* Redirect output structure to the directory. */ 21 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 22 | // "composite": true, /* Enable project compilation */ 23 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 24 | // "removeComments": true, /* Do not emit comments to output. */ 25 | // "noEmit": true, /* Do not emit outputs. */ 26 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 27 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 28 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 29 | 30 | /* Strict Type-Checking Options */ 31 | "strict": true, /* Enable all strict type-checking options. */ 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 46 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 47 | 48 | /* Module Resolution Options */ 49 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 50 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 51 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 52 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 53 | // "typeRoots": [], /* List of folders to include type definitions from. */ 54 | // "types": [], /* Type declaration files to be included in compilation. */ 55 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 56 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 57 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 58 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 68 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 69 | 70 | /* Advanced Options */ 71 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 72 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /code/clientjs/webpack.common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | entry: "./src/index.ts", 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: "ts-loader", 11 | exclude: /node_modules/, 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: [".tsx", ".ts", ".js"], 17 | }, 18 | output: { 19 | path: path.resolve(__dirname, "dist"), 20 | filename: "ClientHooks.js", 21 | // Set this to your namespace e.g. cds.ClientHooks 22 | library: ["cds", "ClientHooks"], 23 | libraryTarget: "var", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /code/clientjs/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { merge } = require("webpack-merge"); 3 | const common = require("./webpack.common.js"); 4 | module.exports = merge(common, { 5 | mode: "development", 6 | devtool: "eval-source-map", 7 | }); 8 | -------------------------------------------------------------------------------- /code/clientjs/webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { merge } = require("webpack-merge"); 3 | const common = require("./webpack.common.js"); 4 | module.exports = merge(common, { 5 | mode: "production", 6 | }); 7 | -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/12b220574659a831a19cd4ce27f1721b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/12b220574659a831a19cd4ce27f1721b.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/14cc9ffc59cf01445c260de0d1316cbf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/14cc9ffc59cf01445c260de0d1316cbf.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/6436517ce7e84bbd405c017843009e9a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/6436517ce7e84bbd405c017843009e9a.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/69b04ab5ead6d00fa0b19b30bf7b7de3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/69b04ab5ead6d00fa0b19b30bf7b7de3.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/a5877389423c4d374c24316677dd3ad7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/a5877389423c4d374c24316677dd3ad7.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/a963fc4c5173f789a6814e8509f4db41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/a963fc4c5173f789a6814e8509f4db41.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/c85dffc07d60c8a9603622b7e19f0dea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/c85dffc07d60c8a9603622b7e19f0dea.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/fa7db755ef5f0c85ac9c39961ded15d9-1621038041728.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/fa7db755ef5f0c85ac9c39961ded15d9-1621038041728.png -------------------------------------------------------------------------------- /media/Part 1 - Setup VSCode/fb2dab9465e077f5beaefdf4b3979e00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 1 - Setup VSCode/fb2dab9465e077f5beaefdf4b3979e00.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/48230f27d21dfff8371dbb8b915e5ab9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/48230f27d21dfff8371dbb8b915e5ab9.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/797a523f367ae1140cded6a2929045a3-1621037998274.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/797a523f367ae1140cded6a2929045a3-1621037998274.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/809d9b6210b8f2d9d1116fc880e0fceb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/809d9b6210b8f2d9d1116fc880e0fceb.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/9c467439ede47c447d1c33150aad3dc8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/9c467439ede47c447d1c33150aad3dc8.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/c6e0e091c1f9bead418037ff47c7c60d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/c6e0e091c1f9bead418037ff47c7c60d.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/da2f58d327dbcbd892a6ed9626f87d7b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/da2f58d327dbcbd892a6ed9626f87d7b.png -------------------------------------------------------------------------------- /media/Part 2 - Webpack/image-20210514171906706.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 2 - Webpack/image-20210514171906706.png -------------------------------------------------------------------------------- /media/Part 3 - Unit Testing/23874f760132e818a03f03b510acf846.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 3 - Unit Testing/23874f760132e818a03f03b510acf846.png -------------------------------------------------------------------------------- /media/Part 3 - Unit Testing/87d87b9bb8fc36d500891cd02ecc700f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 3 - Unit Testing/87d87b9bb8fc36d500891cd02ecc700f.png -------------------------------------------------------------------------------- /media/Part 3 - Unit Testing/8ef4aa8ab2fda103b8834df49da58365-1621039166048-1621039464464.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 3 - Unit Testing/8ef4aa8ab2fda103b8834df49da58365-1621039166048-1621039464464.png -------------------------------------------------------------------------------- /media/Part 3 - Unit Testing/image-20210514173920133.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 3 - Unit Testing/image-20210514173920133.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/1be8153c41337664be9fcf5f9fe88f53-1621041352659.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/1be8153c41337664be9fcf5f9fe88f53-1621041352659.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/209f8c510960f8376781f1dc98624a25-1621041294187.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/209f8c510960f8376781f1dc98624a25-1621041294187.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/2d9691975a3925b38d5183bcf4bdc143-1621041304252.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/2d9691975a3925b38d5183bcf4bdc143-1621041304252.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/8df3f8e76e8e6caaac7e72ca34f51802-1621040814229-1621041273678.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/8df3f8e76e8e6caaac7e72ca34f51802-1621040814229-1621041273678.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/a8d3ba6ad377f718710bfe32264ecb2b-1621040812317-1621041288383.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/a8d3ba6ad377f718710bfe32264ecb2b-1621040812317-1621041288383.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/ab3a009fb2a013645d6c913ac8b2ca96-1621041308204-1621041350541.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/ab3a009fb2a013645d6c913ac8b2ca96-1621041308204-1621041350541.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/c90950766b208d54393f75095d4eee82-1621041314360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/c90950766b208d54393f75095d4eee82-1621041314360.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/d156b381057756735ead6da6c506b00d-1621041291312.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/d156b381057756735ead6da6c506b00d-1621041291312.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/d609aacde1ede48cda732dd35e8ba0b8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/d609aacde1ede48cda732dd35e8ba0b8.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/f6b364cb67673f9f9347ad50d601a6df-1621041312686.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/f6b364cb67673f9f9347ad50d601a6df-1621041312686.png -------------------------------------------------------------------------------- /media/Part 4 - Deploying and browser debugging/faa5aeb1be596978bf2ddfc62bfb7715-1621041310845.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 4 - Deploying and browser debugging/faa5aeb1be596978bf2ddfc62bfb7715-1621041310845.png -------------------------------------------------------------------------------- /media/Part 5 - Earlybound Types and the WebApi/02a54b090272a722073f769cc3e96466.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 5 - Earlybound Types and the WebApi/02a54b090272a722073f769cc3e96466.png -------------------------------------------------------------------------------- /media/Part 5 - Earlybound Types and the WebApi/a577fec6dc0cef026a376e47808783fb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 5 - Earlybound Types and the WebApi/a577fec6dc0cef026a376e47808783fb.png -------------------------------------------------------------------------------- /media/Part 5 - Earlybound Types using dataverse-ify/d00ae9a1bdcd9741a805cb7f4cdb3cc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 5 - Earlybound Types using dataverse-ify/d00ae9a1bdcd9741a805cb7f4cdb3cc1.png -------------------------------------------------------------------------------- /media/Part 5 - Earlybound Types using dataverse-ify/df10f5ab8582f9ffcb51b388e7fdda7e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 5 - Earlybound Types using dataverse-ify/df10f5ab8582f9ffcb51b388e7fdda7e.png -------------------------------------------------------------------------------- /media/Part 7 - Calling JavaScript from a Command Bar Button/1dd3f66d3ab0cd6cec3cefbc49c5275b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 7 - Calling JavaScript from a Command Bar Button/1dd3f66d3ab0cd6cec3cefbc49c5275b.png -------------------------------------------------------------------------------- /media/Part 7 - Calling JavaScript from a Command Bar Button/848470f7cc006b927e03460a3a153061.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 7 - Calling JavaScript from a Command Bar Button/848470f7cc006b927e03460a3a153061.png -------------------------------------------------------------------------------- /media/Part 7 - Calling JavaScript from a Command Bar Button/85bc10820f16cbdb4c1a6c18d4092a98.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 7 - Calling JavaScript from a Command Bar Button/85bc10820f16cbdb4c1a6c18d4092a98.png -------------------------------------------------------------------------------- /media/Part 7 - Calling JavaScript from a Command Bar Button/image-20210516124244333.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 7 - Calling JavaScript from a Command Bar Button/image-20210516124244333.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524113658317.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524113658317.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524114240721.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524114240721.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524114617775.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524114617775.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524114936748.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524114936748.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524115041856.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524115041856.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524120210899.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524120210899.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524120919154.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524120919154.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524135145168.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524135145168.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524135303186.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524135303186.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524140229468.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524140229468.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524141338633.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524141338633.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524142141829.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524142141829.png -------------------------------------------------------------------------------- /media/Part 9 - Calling Custom APIs from TypeScript/image-20210524143025405.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottdurow/building-power-apps-js-webresources/fdd8b202b1285d0befb1b4e08039e5535eaabf50/media/Part 9 - Calling Custom APIs from TypeScript/image-20210524143025405.png --------------------------------------------------------------------------------