├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── config-overrides.js ├── dist ├── asset-manifest.json ├── css │ ├── SuperHeroStyles.css │ └── SuperHeroStyles.css.map ├── index.html └── js │ ├── SuperHeroScript.js │ ├── SuperHeroScript.js.LICENSE.txt │ └── SuperHeroScript.js.map ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.css ├── App.tsx ├── components │ └── hero-picker.tsx ├── helper │ ├── api-helper.tsx │ ├── data-helper.tsx │ └── hero-helper.tsx ├── index.tsx └── react-app-env.d.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "printWidth": 120, 6 | "tabWidth": 2, 7 | "endOfLine":"auto" 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Danish Naglekar 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React App For Dataverse 2 | 3 | This sample project describes how to create React app that can be utilized in a Dataverse solution using supported methods. 4 | 5 | ## Initialize React App 6 | 7 | Initialize React app with a TypeScript template using the following command: 8 | 9 | `create-react-app heros --template typescript` 10 | 11 | ## Install and Configure Prettier 12 | 13 | Install Prettier that will enhance your coding experience. Use the following command to install prettier from `npm`: 14 | 15 | `npm install prettier --save-dev --save-exact` 16 | 17 | We do not want to install this as an app dependency but we need this package during development; so use `--save-dev`. Also, each prettier version may have different styling guides so not to have inconsistent changes across all developers recommended is to install a specific version whenever the app packages are restored; hence I have used `--save-exact`. 18 | 19 | Next, to configure Prettier, I have added `.prettierrc.json` file. Check out the contents of that file and adjust it to your liking. 20 | 21 | ## Install and Configure React App Rewired 22 | 23 | React App Rewired helps you override React App's WebPack config without ejecting. To install react-app-rewired use the following command: 24 | 25 | `npm install react-app-rewired --save-dev` 26 | 27 | Again, as this will be required while building a production app it becomes a dev dependency; hence need to use `--save-dev`. 28 | 29 | Next, to configure React App Rewired, I have added `config-overrides.js` file. Check out the contents of that file. This file is the one that will be responsible for overriding the webpack config file. So, when using it yourself, make sure to do necessary changes in this file. 30 | This confif file will change the `build` folder name to `dist`. It will make sure no chunk files are created and rename the JavaScript and CSS files accordingly. 31 | 32 | Finally, you will need to change the scripts mentioned in the `package.json` file as follows: 33 | 34 | ```json 35 | "scripts": { 36 | "start": "react-app-rewired start", 37 | "build": "react-app-rewired build", 38 | "test": "react-app-rewired test" 39 | } 40 | ``` 41 | 42 | In this we have replaced `react-scripts` with `react-app-rewired` command. 43 | 44 | ## Install Fluent UI 45 | 46 | Fluent UI is a Microsoft UX framework that provides controls which looks similar to the controls used in model-driven app. Hence it made sense to use this library to create my React app. To install this package execute the following command: 47 | 48 | `npm install @fluentui/react` 49 | 50 | ## Develop and Build React App 51 | 52 | Now, that we have configured everything. Develop your React App or Fork this repo to understand the code. Once your app is ready to be deployed to Dataverse; run the `npm run build` command. This will build a production files with the following structure: 53 | 54 | ``` 55 | dist > css > SuperHeroStyles.css 56 | dist > js > SuperHeroScript.js 57 | dist > index.html 58 | ``` 59 | 60 | These files can be deployed to Dataverse as 3 different resources. 61 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | paths: function (paths, env) { 5 | paths.appBuild = paths.appBuild.replace("build", "dist"); 6 | return paths; 7 | }, 8 | webpack: function (config, env) { 9 | if (env === "production") { 10 | config.optimization.runtimeChunk = false; 11 | config.optimization.splitChunks = { 12 | cacheGroups: { 13 | default: false 14 | } 15 | }; 16 | 17 | //JS Overrides 18 | config.output.filename = 'js/SuperHeroScript.js'; 19 | config.output.chunkFilename = 'js/SuperHeroScript.chunk.js'; 20 | 21 | //CSS Overrides 22 | config.plugins[5].options.filename = 'css/SuperHeroStyles.css'; 23 | config.plugins[5].options.moduleFilename = () => 'css/SuperHeroStyles.css'; 24 | 25 | // //Media and Assets Overrides 26 | // config.module.rules[1].oneOf[0].options.name = 'media/[name].[ext]'; 27 | // config.module.rules[1].oneOf[3].options.name = 'media/[name].[ext]'; 28 | } 29 | 30 | return config; 31 | } 32 | } -------------------------------------------------------------------------------- /dist/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "./css/SuperHeroStyles.css", 4 | "main.js": "./js/SuperHeroScript.js", 5 | "main.js.map": "./js/SuperHeroScript.js.map", 6 | "css/SuperHeroStyles.css.map": "./css/SuperHeroStyles.css.map", 7 | "index.html": "./index.html", 8 | "js/SuperHeroScript.js.LICENSE.txt": "./js/SuperHeroScript.js.LICENSE.txt" 9 | }, 10 | "entrypoints": [ 11 | "css/SuperHeroStyles.css", 12 | "js/SuperHeroScript.js" 13 | ] 14 | } -------------------------------------------------------------------------------- /dist/css/SuperHeroStyles.css: -------------------------------------------------------------------------------- 1 | :root{--d:700ms;--e:cubic-bezier(0.19,1,0.22,1);--font-sans:"Rubik",sans-serif;--font-serif:"Cardo",serif}.mru-cards{-webkit-flex-flow:row;flex-flow:row}.card,.mru-cards{display:-webkit-flex;display:flex}.card{margin:10px;position:relative;-webkit-align-items:flex-end;align-items:flex-end;overflow:hidden;padding:1rem;width:350px;text-align:center;color:#f5f5f5;background-color:#f5f5f5;box-shadow:0 1px 1px rgba(0,0,0,.1),0 2px 2px rgba(0,0,0,.1),0 4px 4px rgba(0,0,0,.1),0 8px 8px rgba(0,0,0,.1),0 16px 16px rgba(0,0,0,.1)}@media (min-width:600px){.card{height:350px}}.card:before{height:110%;background-size:cover;background-position:0 0;transition:-webkit-transform 1.05s cubic-bezier(.19,1,.22,1);transition:transform 1.05s cubic-bezier(.19,1,.22,1);transition:transform 1.05s cubic-bezier(.19,1,.22,1),-webkit-transform 1.05s cubic-bezier(.19,1,.22,1);transition:-webkit-transform calc(var(--d)*1.5) var(--e);transition:transform calc(var(--d)*1.5) var(--e);transition:transform calc(var(--d)*1.5) var(--e),-webkit-transform calc(var(--d)*1.5) var(--e)}.card:after,.card:before{content:"";position:absolute;top:0;left:0;width:100%;pointer-events:none}.card:after{display:block;height:200%;background-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.009) 11.7%,rgba(0,0,0,.034) 22.1%,rgba(0,0,0,.072) 31.2%,rgba(0,0,0,.123) 39.4%,rgba(0,0,0,.182) 46.6%,rgba(0,0,0,.249) 53.1%,rgba(0,0,0,.32) 58.9%,rgba(0,0,0,.394) 64.3%,rgba(0,0,0,.468) 69.3%,rgba(0,0,0,.54) 74.1%,rgba(0,0,0,.607) 78.8%,rgba(0,0,0,.668) 83.6%,rgba(0,0,0,.721) 88.7%,rgba(0,0,0,.762) 94.1%,rgba(0,0,0,.79));-webkit-transform:translateY(-50%);transform:translateY(-50%);transition:-webkit-transform 1.4s cubic-bezier(.19,1,.22,1);transition:transform 1.4s cubic-bezier(.19,1,.22,1);transition:transform 1.4s cubic-bezier(.19,1,.22,1),-webkit-transform 1.4s cubic-bezier(.19,1,.22,1);transition:-webkit-transform calc(var(--d)*2) var(--e);transition:transform calc(var(--d)*2) var(--e);transition:transform calc(var(--d)*2) var(--e),-webkit-transform calc(var(--d)*2) var(--e)}.content{position:relative;display:-webkit-flex;display:flex;-webkit-flex-direction:column;flex-direction:column;-webkit-align-items:center;align-items:center;width:100%;padding:1rem;transition:-webkit-transform .7s cubic-bezier(.19,1,.22,1);transition:transform .7s cubic-bezier(.19,1,.22,1);transition:transform .7s cubic-bezier(.19,1,.22,1),-webkit-transform .7s cubic-bezier(.19,1,.22,1);transition:-webkit-transform var(--d) var(--e);transition:transform var(--d) var(--e);transition:transform var(--d) var(--e),-webkit-transform var(--d) var(--e);z-index:1}.content>*+*{margin-top:1rem}.title{font-size:1.3rem;font-weight:700;line-height:1.2}.copy{font-family:"Cardo",serif;font-family:var(--font-serif);font-size:1.125rem;font-style:italic;line-height:1.35}.btn{cursor:pointer;margin-top:1.5rem;padding:.75rem 1.5rem;font-size:.65rem;font-weight:700;letter-spacing:.025rem;text-transform:uppercase;color:#fff;background-color:#000;border:none}.btn:hover{background-color:#0d0d0d}.btn:focus{outline:1px dashed #ff0;outline-offset:3px}@media (hover:hover) and (min-width:600px){.card:after{-webkit-transform:translateY(0);transform:translateY(0)}.content{-webkit-transform:translateY(calc(100% - 4.5rem));transform:translateY(calc(100% - 4.5rem))}.content>:not(.title){opacity:0;-webkit-transform:translateY(1rem);transform:translateY(1rem);transition:opacity .7s cubic-bezier(.19,1,.22,1),-webkit-transform .7s cubic-bezier(.19,1,.22,1);transition:transform .7s cubic-bezier(.19,1,.22,1),opacity .7s cubic-bezier(.19,1,.22,1);transition:transform .7s cubic-bezier(.19,1,.22,1),opacity .7s cubic-bezier(.19,1,.22,1),-webkit-transform .7s cubic-bezier(.19,1,.22,1);transition:opacity var(--d) var(--e),-webkit-transform var(--d) var(--e);transition:transform var(--d) var(--e),opacity var(--d) var(--e);transition:transform var(--d) var(--e),opacity var(--d) var(--e),-webkit-transform var(--d) var(--e)}.card:focus-within,.card:hover{-webkit-align-items:center;align-items:center}.card:focus-within:before,.card:hover:before{-webkit-transform:translateY(-4%);transform:translateY(-4%)}.card:focus-within:after,.card:hover:after{-webkit-transform:translateY(-50%);transform:translateY(-50%)}.card:focus-within .content,.card:hover .content{-webkit-transform:translateY(0);transform:translateY(0)}.card:focus-within .content>:not(.title),.card:hover .content>:not(.title){opacity:1;-webkit-transform:translateY(0);transform:translateY(0);transition-delay:87.5ms;transition-delay:calc(var(--d)/8)}.card:focus-within .content,.card:focus-within .content>:not(.title),.card:focus-within:after,.card:focus-within:before{transition-duration:0s}} 2 | /*# sourceMappingURL=SuperHeroStyles.css.map */ -------------------------------------------------------------------------------- /dist/css/SuperHeroStyles.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://src/App.css"],"names":[],"mappings":"AAAA,MACE,SAAU,CACV,+BAAmC,CACnC,8BAAgC,CAChC,0BACF,CAEA,WAEE,qBAAc,CAAd,aACF,CAEA,iBAJE,oBAAa,CAAb,YAiBF,CAbA,MACE,WAAY,CACZ,iBAAkB,CAElB,4BAAqB,CAArB,oBAAqB,CACrB,eAAgB,CAChB,YAAa,CACb,WAAY,CACZ,iBAAkB,CAClB,aAAiB,CACjB,wBAA4B,CAC5B,yIAEF,CACA,yBACE,MACE,YACF,CACF,CACA,aAME,WAAY,CACZ,qBAAsB,CACtB,uBAAwB,CACxB,4DAAmD,CAAnD,oDAAmD,CAAnD,sGAAmD,CAAnD,wDAAmD,CAAnD,gDAAmD,CAAnD,8FAEF,CACA,yBAXE,UAAW,CACX,iBAAkB,CAClB,KAAM,CACN,MAAO,CACP,UAAW,CAKX,mBAgCF,CA9BA,YAEE,aAAc,CAKd,WAAY,CAEZ,sYAkBC,CACD,kCAA2B,CAA3B,0BAA2B,CAC3B,2DAAiD,CAAjD,mDAAiD,CAAjD,oGAAiD,CAAjD,sDAAiD,CAAjD,8CAAiD,CAAjD,0FACF,CACA,SACE,iBAAkB,CAClB,oBAAa,CAAb,YAAa,CACb,6BAAsB,CAAtB,qBAAsB,CACtB,0BAAmB,CAAnB,kBAAmB,CACnB,UAAW,CACX,YAAa,CACb,0DAAuC,CAAvC,kDAAuC,CAAvC,kGAAuC,CAAvC,8CAAuC,CAAvC,sCAAuC,CAAvC,0EAAuC,CACvC,SACF,CACA,aACE,eACF,CAEA,OACE,gBAAiB,CACjB,eAAiB,CACjB,eACF,CAEA,MACE,yBAA8B,CAA9B,6BAA8B,CAC9B,kBAAmB,CACnB,iBAAkB,CAClB,gBACF,CAEA,KACE,cAAe,CACf,iBAAkB,CAClB,qBAAuB,CACvB,gBAAkB,CAClB,eAAiB,CACjB,sBAAwB,CACxB,wBAAyB,CACzB,UAAY,CACZ,qBAAuB,CACvB,WACF,CACA,WACE,wBACF,CACA,WACE,uBAA0B,CAC1B,kBACF,CAEA,2CACE,YACE,+BAAwB,CAAxB,uBACF,CAEA,SACE,iDAA0C,CAA1C,yCACF,CACA,sBACE,SAAU,CACV,kCAA2B,CAA3B,0BAA2B,CAC3B,gGAAkE,CAAlE,wFAAkE,CAAlE,wIAAkE,CAAlE,wEAAkE,CAAlE,gEAAkE,CAAlE,oGACF,CAEA,+BAEE,0BAAmB,CAAnB,kBACF,CACA,6CAEE,iCAA0B,CAA1B,yBACF,CACA,2CAEE,kCAA2B,CAA3B,0BACF,CACA,iDAEE,+BAAwB,CAAxB,uBACF,CACA,2EAEE,SAAU,CACV,+BAAwB,CAAxB,uBAAwB,CACxB,uBAAoC,CAApC,iCACF,CAEA,wHAIE,sBACF,CACF","file":"SuperHeroStyles.css","sourcesContent":[":root {\n --d: 700ms;\n --e: cubic-bezier(0.19, 1, 0.22, 1);\n --font-sans: \"Rubik\", sans-serif;\n --font-serif: \"Cardo\", serif;\n}\n\n.mru-cards {\n display: flex;\n flex-flow: row;\n}\n\n.card {\n margin: 10px;\n position: relative;\n display: flex;\n align-items: flex-end;\n overflow: hidden;\n padding: 1rem;\n width: 350px;\n text-align: center;\n color: whitesmoke;\n background-color: whitesmoke;\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 2px 2px rgba(0, 0, 0, 0.1), 0 4px 4px rgba(0, 0, 0, 0.1),\n 0 8px 8px rgba(0, 0, 0, 0.1), 0 16px 16px rgba(0, 0, 0, 0.1);\n}\n@media (min-width: 600px) {\n .card {\n height: 350px;\n }\n}\n.card:before {\n content: \"\";\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 110%;\n background-size: cover;\n background-position: 0 0;\n transition: transform calc(var(--d) * 1.5) var(--e);\n pointer-events: none;\n}\n.card:after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 200%;\n pointer-events: none;\n background-image: linear-gradient(\n to bottom,\n rgba(0, 0, 0, 0) 0%,\n rgba(0, 0, 0, 0.009) 11.7%,\n rgba(0, 0, 0, 0.034) 22.1%,\n rgba(0, 0, 0, 0.072) 31.2%,\n rgba(0, 0, 0, 0.123) 39.4%,\n rgba(0, 0, 0, 0.182) 46.6%,\n rgba(0, 0, 0, 0.249) 53.1%,\n rgba(0, 0, 0, 0.32) 58.9%,\n rgba(0, 0, 0, 0.394) 64.3%,\n rgba(0, 0, 0, 0.468) 69.3%,\n rgba(0, 0, 0, 0.54) 74.1%,\n rgba(0, 0, 0, 0.607) 78.8%,\n rgba(0, 0, 0, 0.668) 83.6%,\n rgba(0, 0, 0, 0.721) 88.7%,\n rgba(0, 0, 0, 0.762) 94.1%,\n rgba(0, 0, 0, 0.79) 100%\n );\n transform: translateY(-50%);\n transition: transform calc(var(--d) * 2) var(--e);\n}\n.content {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 100%;\n padding: 1rem;\n transition: transform var(--d) var(--e);\n z-index: 1;\n}\n.content > * + * {\n margin-top: 1rem;\n}\n\n.title {\n font-size: 1.3rem;\n font-weight: bold;\n line-height: 1.2;\n}\n\n.copy {\n font-family: var(--font-serif);\n font-size: 1.125rem;\n font-style: italic;\n line-height: 1.35;\n}\n\n.btn {\n cursor: pointer;\n margin-top: 1.5rem;\n padding: 0.75rem 1.5rem;\n font-size: 0.65rem;\n font-weight: bold;\n letter-spacing: 0.025rem;\n text-transform: uppercase;\n color: white;\n background-color: black;\n border: none;\n}\n.btn:hover {\n background-color: #0d0d0d;\n}\n.btn:focus {\n outline: 1px dashed yellow;\n outline-offset: 3px;\n}\n\n@media (hover: hover) and (min-width: 600px) {\n .card:after {\n transform: translateY(0);\n }\n\n .content {\n transform: translateY(calc(100% - 4.5rem));\n }\n .content > *:not(.title) {\n opacity: 0;\n transform: translateY(1rem);\n transition: transform var(--d) var(--e), opacity var(--d) var(--e);\n }\n\n .card:hover,\n .card:focus-within {\n align-items: center;\n }\n .card:hover:before,\n .card:focus-within:before {\n transform: translateY(-4%);\n }\n .card:hover:after,\n .card:focus-within:after {\n transform: translateY(-50%);\n }\n .card:hover .content,\n .card:focus-within .content {\n transform: translateY(0);\n }\n .card:hover .content > *:not(.title),\n .card:focus-within .content > *:not(.title) {\n opacity: 1;\n transform: translateY(0);\n transition-delay: calc(var(--d) / 8);\n }\n\n .card:focus-within:before,\n .card:focus-within:after,\n .card:focus-within .content,\n .card:focus-within .content > *:not(.title) {\n transition-duration: 0s;\n }\n}\n"]} -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /dist/js/SuperHeroScript.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! ***************************************************************************** 8 | Copyright (c) Microsoft Corporation. 9 | 10 | Permission to use, copy, modify, and/or distribute this software for any 11 | purpose with or without fee is hereby granted. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 14 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 15 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 17 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 18 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | PERFORMANCE OF THIS SOFTWARE. 20 | ***************************************************************************** */ 21 | 22 | /** @license React v0.20.2 23 | * scheduler.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.2 32 | * react-dom.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.2 41 | * react-jsx-runtime.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | /** @license React v17.0.2 50 | * react.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heros", 3 | "homepage": "./", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@fluentui/react": "^8.9.2", 8 | "@testing-library/jest-dom": "^5.11.10", 9 | "@testing-library/react": "^11.2.6", 10 | "@testing-library/user-event": "^12.8.3", 11 | "@types/jest": "^26.0.22", 12 | "@types/node": "^12.20.7", 13 | "@types/react": "^17.0.3", 14 | "@types/react-dom": "^17.0.3", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-scripts": "4.0.3", 18 | "typescript": "^4.2.4", 19 | "web-vitals": "^1.1.1" 20 | }, 21 | "scripts": { 22 | "start": "react-app-rewired start", 23 | "build": "react-app-rewired build", 24 | "test": "react-app-rewired test" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "devDependencies": { 45 | "prettier": "2.2.1", 46 | "react-app-rewired": "^2.1.8" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React App 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --d: 700ms; 3 | --e: cubic-bezier(0.19, 1, 0.22, 1); 4 | --font-sans: "Rubik", sans-serif; 5 | --font-serif: "Cardo", serif; 6 | } 7 | 8 | .mru-cards { 9 | display: flex; 10 | flex-flow: row; 11 | flex-wrap: wrap; 12 | } 13 | 14 | .card { 15 | margin: 10px; 16 | position: relative; 17 | display: flex; 18 | align-items: flex-end; 19 | overflow: hidden; 20 | padding: 1rem; 21 | width: 350px; 22 | text-align: center; 23 | color: whitesmoke; 24 | background-color: whitesmoke; 25 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1), 0 2px 2px rgba(0, 0, 0, 0.1), 0 4px 4px rgba(0, 0, 0, 0.1), 26 | 0 8px 8px rgba(0, 0, 0, 0.1), 0 16px 16px rgba(0, 0, 0, 0.1); 27 | } 28 | @media (min-width: 600px) { 29 | .card { 30 | height: 350px; 31 | } 32 | } 33 | .card:before { 34 | content: ""; 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | width: 100%; 39 | height: 110%; 40 | background-size: cover; 41 | background-position: 0 0; 42 | transition: transform calc(var(--d) * 1.5) var(--e); 43 | pointer-events: none; 44 | } 45 | .card:after { 46 | content: ""; 47 | display: block; 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | width: 100%; 52 | height: 200%; 53 | pointer-events: none; 54 | background-image: linear-gradient( 55 | to bottom, 56 | rgba(0, 0, 0, 0) 0%, 57 | rgba(0, 0, 0, 0.009) 11.7%, 58 | rgba(0, 0, 0, 0.034) 22.1%, 59 | rgba(0, 0, 0, 0.072) 31.2%, 60 | rgba(0, 0, 0, 0.123) 39.4%, 61 | rgba(0, 0, 0, 0.182) 46.6%, 62 | rgba(0, 0, 0, 0.249) 53.1%, 63 | rgba(0, 0, 0, 0.32) 58.9%, 64 | rgba(0, 0, 0, 0.394) 64.3%, 65 | rgba(0, 0, 0, 0.468) 69.3%, 66 | rgba(0, 0, 0, 0.54) 74.1%, 67 | rgba(0, 0, 0, 0.607) 78.8%, 68 | rgba(0, 0, 0, 0.668) 83.6%, 69 | rgba(0, 0, 0, 0.721) 88.7%, 70 | rgba(0, 0, 0, 0.762) 94.1%, 71 | rgba(0, 0, 0, 0.79) 100% 72 | ); 73 | transform: translateY(-50%); 74 | transition: transform calc(var(--d) * 2) var(--e); 75 | } 76 | .content { 77 | position: relative; 78 | display: flex; 79 | flex-direction: column; 80 | align-items: center; 81 | width: 100%; 82 | padding: 1rem; 83 | transition: transform var(--d) var(--e); 84 | z-index: 1; 85 | } 86 | .content > * + * { 87 | margin-top: 1rem; 88 | } 89 | 90 | .title { 91 | font-size: 1.3rem; 92 | font-weight: bold; 93 | line-height: 1.2; 94 | } 95 | 96 | .copy { 97 | font-family: var(--font-serif); 98 | font-size: 1.125rem; 99 | font-style: italic; 100 | line-height: 1.35; 101 | } 102 | 103 | .btn { 104 | cursor: pointer; 105 | margin-top: 1.5rem; 106 | padding: 0.75rem 1.5rem; 107 | font-size: 0.65rem; 108 | font-weight: bold; 109 | letter-spacing: 0.025rem; 110 | text-transform: uppercase; 111 | color: white; 112 | background-color: black; 113 | border: none; 114 | } 115 | .btn:hover { 116 | background-color: #0d0d0d; 117 | } 118 | .btn:focus { 119 | outline: 1px dashed yellow; 120 | outline-offset: 3px; 121 | } 122 | 123 | @media (hover: hover) and (min-width: 600px) { 124 | .card:after { 125 | transform: translateY(0); 126 | } 127 | 128 | .content { 129 | transform: translateY(calc(100% - 4.5rem)); 130 | } 131 | .content > *:not(.title) { 132 | opacity: 0; 133 | transform: translateY(1rem); 134 | transition: transform var(--d) var(--e), opacity var(--d) var(--e); 135 | } 136 | 137 | .card:hover, 138 | .card:focus-within { 139 | align-items: center; 140 | } 141 | .card:hover:before, 142 | .card:focus-within:before { 143 | transform: translateY(-4%); 144 | } 145 | .card:hover:after, 146 | .card:focus-within:after { 147 | transform: translateY(-50%); 148 | } 149 | .card:hover .content, 150 | .card:focus-within .content { 151 | transform: translateY(0); 152 | } 153 | .card:hover .content > *:not(.title), 154 | .card:focus-within .content > *:not(.title) { 155 | opacity: 1; 156 | transform: translateY(0); 157 | transition-delay: calc(var(--d) / 8); 158 | } 159 | 160 | .card:focus-within:before, 161 | .card:focus-within:after, 162 | .card:focus-within .content, 163 | .card:focus-within .content > *:not(.title) { 164 | transition-duration: 0s; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import HeroPicker from "./components/hero-picker"; 4 | 5 | function App() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /src/components/hero-picker.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | // import ReactDOM from "react-dom"; 3 | import { IPersonaProps, Persona, PersonaSize, PersonaPresence } from "@fluentui/react/lib/Persona"; 4 | import { IBasePickerSuggestionsProps, NormalPeoplePicker } from "@fluentui/react/lib/Pickers"; 5 | import { Text } from "@fluentui/react/lib/Text"; 6 | import { MessageBar } from "@fluentui/react/lib/MessageBar"; 7 | import { Icon } from "@fluentui/react/lib/Icon"; 8 | import { PrimaryButton } from "@fluentui/react/lib/Button"; 9 | import { Panel } from "@fluentui/react/lib/Panel"; 10 | import { TextField } from "@fluentui/react/lib/TextField"; 11 | import { Separator } from "@fluentui/react/lib/Separator"; 12 | import { initializeIcons } from "@fluentui/react/lib/Icons"; 13 | import { createTheme, ITheme } from "@fluentui/react/lib/Styling"; 14 | import { useBoolean } from "@fluentui/react-hooks"; 15 | import ApiHelper from "../helper/api-helper"; 16 | import HeroHelper from "../helper/hero-helper"; 17 | 18 | initializeIcons(undefined, { disableWarnings: true }); 19 | 20 | export interface IMainProps { 21 | text: string; 22 | } 23 | 24 | const marginStyles = { 25 | root: { 26 | margin: 10, 27 | }, 28 | }; 29 | 30 | const theme: ITheme = createTheme({ 31 | fonts: { 32 | medium: { 33 | fontFamily: "Monaco, Menlo, Consolas", 34 | fontSize: "30px", 35 | }, 36 | }, 37 | }); 38 | 39 | const suggestionProps: IBasePickerSuggestionsProps = { 40 | suggestionsHeaderText: "Suggested People", 41 | mostRecentlyUsedHeaderText: "Suggested Contacts", 42 | noResultsFoundText: "No results found", 43 | loadingText: "Loading", 44 | showRemoveButtons: true, 45 | suggestionsAvailableAlertText: "People Picker Suggestions available", 46 | suggestionsContainerAriaLabel: "Suggested contacts", 47 | }; 48 | 49 | const HeroPicker = (props: IMainProps): JSX.Element => { 50 | const [heroPersonaList, setHeroPersonaList] = React.useState([]); 51 | const [heroPersona, setHeroPersona] = React.useState({}); 52 | const [heroList, setHeroList] = React.useState([]); 53 | const [selectedHero, setSelectedHero] = React.useState(); 54 | const [mruSuperHero, setMRUSuperHero] = React.useState([]); 55 | const [mruSuperHeroPersona, setMRUSuperHeroPersona] = React.useState([]); 56 | const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false); 57 | 58 | useEffect(() => { 59 | const fetchHeros = async () => { 60 | let data: any[] = await HeroHelper.GetAllHeros(); 61 | setHeroList(data.map((en) => en)); 62 | setHeroPersonaList( 63 | data.map((en) => { 64 | let persona: IPersonaProps = {}; 65 | persona.text = en.pmav_name; 66 | persona.secondaryText = en.pmav_publisher; 67 | return persona; 68 | }), 69 | ); 70 | }; 71 | fetchHeros(); 72 | }, []); 73 | 74 | const onFilterChanged = ( 75 | filterText: string, 76 | currentPersonas: IPersonaProps[] | undefined, 77 | limitResults?: number, 78 | ): IPersonaProps[] | Promise => { 79 | if (filterText) { 80 | let filteredPersonas: IPersonaProps[] = filterPersonasByText(filterText); 81 | 82 | filteredPersonas = removeDuplicates(filteredPersonas, currentPersonas); 83 | filteredPersonas = limitResults ? filteredPersonas.slice(0, limitResults) : filteredPersonas; 84 | 85 | return filteredPersonas; 86 | } else { 87 | return []; 88 | } 89 | }; 90 | 91 | const onItemSelected = async (item: IPersonaProps | undefined): Promise => { 92 | if (item) { 93 | setHeroPersona(item); 94 | 95 | let filteredHeros = heroList.filter((h) => h.pmav_name === (item.text ?? "")); 96 | setSelectedHero(filteredHeros[0]); 97 | setMRUSuperHero((sh) => [...sh, filteredHeros[0].pmav_name]); 98 | populateMRUs(); 99 | } 100 | return new Promise((resolve, reject) => item); 101 | }; 102 | 103 | const changeSelectedHero = (shName: string) => { 104 | let filteredHeros = heroList.filter((h) => h.pmav_name === (shName ?? "")); 105 | setSelectedHero(filteredHeros[0]); 106 | openPanel(); 107 | }; 108 | 109 | const showHeroDetails = () => { 110 | let filteredHeros = heroList.filter((h) => h.pmav_name === (heroPersona.text ?? "")); 111 | setSelectedHero(filteredHeros[0]); 112 | openPanel(); 113 | }; 114 | 115 | const filterPersonasByText = (filterText: string): IPersonaProps[] => { 116 | return heroPersonaList.filter((item) => doesTextStartWith(item.text as string, filterText)); 117 | }; 118 | 119 | const populateMRUs = () => { 120 | setMRUSuperHeroPersona([]); 121 | mruSuperHero?.map(async (sh: string) => { 122 | let filteredHero = heroList.filter((h) => h.pmav_name === (sh ?? ""))[0]; 123 | let shUrl = await ApiHelper.GetSuperHeroImageUrl(sh); 124 | 125 | let cardStyle = { 126 | backgroundImage: "url(" + shUrl + ")", 127 | backgroundPosition: "center", 128 | }; 129 | 130 | let card = ( 131 |
132 |
133 |

{sh}

134 |

135 | {sh} was created by {filteredHero.pmav_publisher} publisher. 136 |

137 | 145 |
146 |
147 | ); 148 | 149 | setMRUSuperHeroPersona((mru) => [...mru, card]); 150 | }); 151 | }; 152 | 153 | return ( 154 | <> 155 | This is a demo app on how to create React app for Dataverse in a supported way. 156 |
157 | 158 | {props.text} 159 | 160 | ) => console.log("onBlur called"), 167 | onFocus: (ev: React.FocusEvent) => console.log("onFocus called"), 168 | "aria-label": "Hero Picker", 169 | }} 170 | onItemSelected={onItemSelected} 171 | resolveDelay={300} 172 | styles={marginStyles} 173 | /> 174 |
175 | 182 | 183 |
184 |

Most Recently Searched Super Heros

185 |
186 |
{mruSuperHeroPersona}
187 | 194 | 195 | {selectedHero?.pmav_name ?? "--"} 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | Stats 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | ); 214 | }; 215 | 216 | function onRenderSecondaryText(props: IPersonaProps | undefined): JSX.Element { 217 | return ( 218 |
219 | {props?.secondaryText} 220 |
221 | ); 222 | } 223 | 224 | function removeDuplicates(personas: IPersonaProps[], possibleDupes: IPersonaProps[] | undefined) { 225 | return personas.filter((persona) => !listContainsPersona(persona, possibleDupes)); 226 | } 227 | 228 | function listContainsPersona(persona: IPersonaProps, personas: IPersonaProps[] | undefined) { 229 | if (!personas || !personas.length || personas.length === 0) { 230 | return false; 231 | } 232 | return personas.filter((item) => item.text === persona.text).length > 0; 233 | } 234 | 235 | function doesTextStartWith(text: string, filterText: string): boolean { 236 | return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0; 237 | } 238 | 239 | export default HeroPicker; 240 | -------------------------------------------------------------------------------- /src/helper/api-helper.tsx: -------------------------------------------------------------------------------- 1 | export default class ApiHelper { 2 | /** 3 | * GetSuperHeroImageUrl will retrieve the super hero image from custom API 4 | */ 5 | public static async GetSuperHeroImageUrl(superheroName: string): Promise { 6 | let headers = new Headers(); 7 | headers.append("Content-Type", "application/json"); 8 | headers.append("Access-Control-Allow-Origin", "https://powermaverick.crm.dynamics.com"); 9 | headers.append("Access-Control-Allow-Credentials", "true"); 10 | 11 | let raw = JSON.stringify({ 12 | name: superheroName, 13 | }); 14 | 15 | let requestOptions: RequestInit = { 16 | method: "POST", 17 | mode: "cors", 18 | headers: headers, 19 | body: raw, 20 | redirect: "follow", 21 | }; 22 | 23 | let shData = await fetch("https://maverick-superheros.azurewebsites.net/superhero", requestOptions); 24 | let shJsonData = await shData.json(); 25 | 26 | return shJsonData.url; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/helper/data-helper.tsx: -------------------------------------------------------------------------------- 1 | export default class DataHelper { 2 | /** 3 | * Fetches the data from Dataverse based on provided FetchXML 4 | */ 5 | public static Fetch(fetchXml: string): any[] { 6 | let values: any[] = []; 7 | 8 | var xmlParser = new DOMParser(); 9 | var parsedFetchXml = xmlParser.parseFromString(fetchXml, "text/xml"); 10 | var entityName = (parsedFetchXml.getElementsByTagName("entity")[0] as any).attributes["name"].value; 11 | 12 | var encodedFetchXml = encodeURIComponent(fetchXml); 13 | 14 | return this.GetXrmContext() 15 | .WebApi.retrieveMultipleRecords(entityName, "?fetchXml=" + encodedFetchXml) 16 | .then( 17 | (results: any) => { 18 | let formattedResults = this.FormatRecords(results.entities); 19 | values = values.concat(formattedResults); 20 | return values; 21 | }, 22 | function (error: any) { 23 | // TODO 24 | }, 25 | ); 26 | } 27 | 28 | private static GetXrmContext() { 29 | if ((window as any).Xrm) { 30 | return (window as any).Xrm; 31 | } else if ((window as any).opener && (window as any).opener.Xrm) { 32 | // Ribbon button dialogs 33 | return (window as any).opener.Xrm; 34 | } else if ((window as any).parent && (window as any).parent.Xrm) { 35 | // Dashboard & sitemap standalone pages 36 | return (window as any).parent.Xrm; 37 | } else if ((window as any).opener && (window as any).opener.parent && (window as any).opener.parent.Xrm) { 38 | // Tree view dialogs (dialog opened from a form-hosted web resource) 39 | return (window as any).opener.parent.Xrm; 40 | } else if ((window as any).opener && (window as any).opener.opener && (window as any).opener.opener.Xrm) { 41 | // Dialog in a dialog (account i/s changes on save) 42 | return (window as any).opener.opener.Xrm; 43 | } 44 | } 45 | 46 | private static FormatRecords(records: any) { 47 | let formattedResults: any[] = []; 48 | 49 | if (records.length > 0) { 50 | for (var i = 0; i < records.length; i++) { 51 | let currentResult = records[i]; 52 | let formattedResult: any = {}; 53 | for (var key in currentResult) { 54 | if (key === "@odata.etag") { 55 | formattedResult.recordVersion = currentResult[key]; 56 | } else if (key.indexOf("_") === 0) { 57 | // This is a lookup's property 58 | var attribute = key.substring(1, key.indexOf("_value")); 59 | if (!formattedResult[attribute]) { 60 | formattedResult[attribute] = {}; 61 | } 62 | if (this.EndsWith(key, "_value")) { 63 | formattedResult[attribute].Id = currentResult[key]; 64 | } else if (this.EndsWith(key, "_value@OData.Community.Display.V1.FormattedValue")) { 65 | formattedResult[attribute].Name = currentResult[key]; 66 | } else if (this.EndsWith(key, "_value@Microsoft.Dynamics.CRM.lookuplogicalname")) { 67 | formattedResult[attribute].LogicalName = currentResult[key]; 68 | } 69 | } else if (key.indexOf(".") > 0) { 70 | // Aliased values will contain a . after the alias name. Using 0 to ensure we are grabbing an aliased value and not some other odata property starting with '.' 71 | var alias = key.substring(0, key.indexOf(".")); 72 | var aliasedAttribute = key.substring(key.indexOf(".") + 1, key.length); 73 | if (!formattedResult[alias]) { 74 | formattedResult[alias] = {}; 75 | } 76 | 77 | if ( 78 | this.EndsWith(key, "@OData.Community.Display.V1.FormattedValue") || 79 | this.EndsWith(key, "@Microsoft.Dynamics.CRM.lookuplogicalname") || 80 | this.EndsWith(key, "@OData.Community.Display.V1.AttributeName") 81 | ) { 82 | // handle aliased when the id is processed 83 | continue; 84 | } else if (currentResult[key + "@Microsoft.Dynamics.CRM.lookuplogicalname"]) { 85 | // Try to identify if the current key is for a lookup or not. Can't just assume lookups will end with 'id'. 86 | formattedResult[alias][aliasedAttribute] = { 87 | Id: currentResult[key], 88 | Name: currentResult[key + "@OData.Community.Display.V1.FormattedValue"], 89 | LogicalName: currentResult[key + "@Microsoft.Dynamics.CRM.lookuplogicalname"], 90 | }; 91 | } else { 92 | formattedResult[alias][aliasedAttribute] = currentResult[key]; 93 | } 94 | } else { 95 | formattedResult[key] = currentResult[key]; 96 | } 97 | } 98 | formattedResults.push(formattedResult); 99 | } 100 | } 101 | 102 | return formattedResults; 103 | } 104 | 105 | private static EndsWith(str: any, suffix: any) { 106 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/helper/hero-helper.tsx: -------------------------------------------------------------------------------- 1 | import DataHelper from "./data-helper"; 2 | 3 | export default class HeroHelper { 4 | /** 5 | * GetAllHeros will retrieve data from Hero table in Dataverse 6 | */ 7 | public static async GetAllHeros(): Promise { 8 | const fetch = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | `; 28 | const response = await DataHelper.Fetch(fetch); 29 | return response; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root"), 10 | ); 11 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext", 8 | "ES2015" 9 | ], 10 | "sourceMap": true, 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "module": "esnext", 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "jsx": "react-jsx" 24 | }, 25 | "include": [ 26 | "src/**/*" 27 | ], 28 | "exclude": [ 29 | "node_modules", 30 | "dist" 31 | ] 32 | } 33 | --------------------------------------------------------------------------------