├── .github └── CODEOWNERS ├── LICENSE ├── README.md └── sdk-samples ├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .nvmrc ├── config └── setupTests.js ├── extension.js ├── jest.config.js ├── package.json ├── sample.env ├── src ├── cards │ ├── CacheCard.jsx │ ├── CardConfigurationCard.jsx │ ├── DrilldownCard.jsx │ ├── ErrorMessageCard.jsx │ ├── GraphQLQueryCard.jsx │ ├── LoadingStateCard.jsx │ ├── MarkdownTemplate.jsx │ ├── MarkdownTemplateConfig.jsx │ ├── PreventRemoveCard.jsx │ ├── PropsCard.jsx │ └── ThrowErrorCard.jsx ├── i18n │ ├── ar.json │ ├── en-AU.json │ ├── en-GB.json │ ├── en.json │ ├── es.json │ ├── fr-CA.json │ ├── intlUtility.js │ └── manifest.helper.js ├── page │ └── index.jsx ├── test │ ├── cards │ │ ├── CacheCard.test.js │ │ ├── CardConfigurationCard.test.js │ │ ├── ErrorMessageCard.test.js │ │ ├── LoadingStateCard.test.js │ │ ├── MarkdownTemplate.test.js │ │ ├── PropsCard.test.js │ │ ├── PropsPage.test.js │ │ └── ThrowErrorCard.test.js │ └── helpers │ │ └── manifest.helper.test.js └── utils │ ├── ReactIntlProviderWrapper.jsx │ └── test-utils │ ├── enzymeSetup.js │ └── enzymeUtil.js └── webpack.config.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tnadolski-ellucian -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ellucian © 2023 Ellucian Company L.P. and its affiliates 2 | 3 | ## experience-sdk-sample-extensions 4 | This repository is compiled of Experience extension sample cards meant to be used in conjunction with the Ellucian Experience SDK (Software Development Kit). These samples cards are located within the src/cards folder. Intended to be used by developers who want to learn how to build Experience extensions, these examples are provided as a resource to understand how react hooks, props, and UI components behave inside of an extension.  5 | 6 | To learn more about how to use these sample extensions, follow the [Ellucian Toolkit Developer Community Forum](https://elluciansupport.service-now.com/customer_center?id=community_forum&sys_id=e73389abdb4f5c50c23a3cae7c961913) or visit the Ellucian Experience Resource Center.  7 | 8 | ### GraphQLCard 9 | This card demonstrates how GraphQL queries can be executed to retrieve data from Ethos. First, the list of Sites is retrieved and presented to the user. Once the user chooses a site, a second Ethos query retrieves the buildings for that site and displays it in a list. 10 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 11 | The results of the request for sites are stored in the dashboard's cache. See how storeItem is used in the initial useEffect. 12 | The card tells the dashboard to show the skeleton-loading components while the Sites are fetched from Ethos or cache. This is handled by the two calls to setLoadingStatus in the initial useEffect hook. 13 | Multiple languages are supported in this card by use of the ReactIntlProviderWrapper. withIntl is used in the statement exporting this card. The strings to be replaced by local versions are throughout the JSX with calls to intl.formatMessage, passing in id to the string to use and optionally values to use for substituting values. 14 | 15 | This card requires Data Access to be setup, and having the Buildings, Persons, and Sites GraphQL resources assigned to your Ellucian Experience application. 16 | Follow the [Add GraphQL resources to Ellucian Experience](https://resources.elluciancloud.com/bundle/ellucian_experience/page/t_experience_add_graphql_resources.html) section of the Ellucian Experience - Configure documentation for detailed instructions. 17 | 18 | ### CacheCard 19 | Like the GraphQLCard, this uses the cache available to cards. This card will remember how many times this card is loaded. When you press refresh in your browser, this card loads and increments the counter stored in the cache. To remove this value from the cache, hit the Reset button. 20 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 21 | Multiple languages are supported in this card by use of the ReactIntlProviderWrapper. withIntl is used in the statement exporting this card. The strings to be replaced by local versions are throughout the JSX with calls to intl.formatMessage, passing in id to the string to use and optionally values to use for substituting values. 22 | 23 | ### CardConfigurationCard 24 | This card shows all the keys and values stored in the configuration of this extension and card. This is only for demonstration purposes. 25 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 26 | 27 | ### DrilldownCard 28 | Some cards need to show additional data when a user selects an item. This pattern allows the card to show that additional data, with a back button for the user to go back to what they were seeing before. In this example, this card counts how many times the drilldown happens. When the button is tapped, the counter stored in the cache increases. When the counter is changed, the useEffect hook will call drilldown(). The arguments to drilldown() are a callback function to call when the back arrow is tapped and the message to place in the card's header instead of the card's title. The user can go back either by tapping the back arrow provided by the dashboard or tapping the button. Either option will call the same API resetDrilldown(). 29 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 30 | Multiple languages are supported in this card by use of the ReactIntlProviderWrapper. withIntl is used in the statement exporting this card. The strings to be replaced by local versions are throughout the JSX with calls to intl.formatMessage, passing in id to the string to use and optionally values to use for substituting values. 31 | 32 | ### ErrorMessageCard 33 | Sometimes there are conditions where you want to show an error to the user. This may be because the user does not have data or a server is offline. These are paths where the code in the card is behaving correctly and wants to report an error or message to the user. The SDK delivers a standard method to do that so different cards can have a consistent look. This card asks the user for a header message, a text message, an icon, and an icon color. When the form is submitted the standard view is shown. 34 | Normally the developer would make these decisions and tell the dashboard to show this view by calling setErrorMessage. 35 | The names of the icons available to use are in the Ellucian Path Design System documentation under Design Guidelines - Iconography. 36 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 37 | Multiple languages are supported in this card by use of the ReactIntlProviderWrapper. withIntl is used in the statement exporting this card. The strings to be replaced by local versions are throughout the JSX with calls to intl.formatMessage, passing in id to the string to use and optionally values to use for substituting values. 38 | 39 | ### LoadingStateCard 40 | When a card needs time to load, it is best to show something to the user. Instead of different cards providing different behavior in these cases, the SDK provides the setLoadingStatus() function to tell the dashboard to show the standard loading view. In this card, the button will trigger the call to show the loading view for ten seconds and then disappear back to the other content. 41 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 42 | Multiple languages are supported in this card by use of the ReactIntlProviderWrapper. withIntl is used in the statement exporting this card. The strings to be replaced by local versions are throughout the JSX with calls to intl.formatMessage, passing in id to the string to use and optionally values to use for substituting values. 43 | 44 | ### PreventRemoveCard 45 | Users can normally choose to bookmark cards to their dashboard. However, there may be a reason why the user should complete an action before removing the card. This card demonstrates how to disable the 'remove bookmark' button by calling setPreventRemove() and send the message to display in the tooltip in setPreventRemoveMessage(). Normally, this would be the developer writing code on when to trigger calls to setPreventRemove(), and the switch provided in this card is only for demonstration. 46 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 47 | Multiple languages are supported in this card by use of the ReactIntlProviderWrapper. withIntl is used in the statement exporting this card. The strings to be replaced by local versions are throughout the JSX with calls to intl.formatMessage, passing in id to the string to use and optionally values to use for substituting values. 48 | 49 | ### PropsCard 50 | This card shows all the properties passed into the card. This is only for demonstration purposes. 51 | The Ellucian Path Design System is used to create visual components. Check out the returned JSX from this function. The JSS used to style the card is in the styles variable, and the withStyles Higher-Order Component is used in the statement exporting this card. 52 | 53 | ### ThrowErrorCard 54 | If a card behaves badly, the dashboard will remove that card and replace it with a card that tells the user that something went wrong. This card demonstrates what happens when an error occurs. 55 | 56 | ### MarkdownTemplate 57 | This creates a template to make cards from. Templates will appear under dashboard's "Add New" menu in the configuration page. Child cards created through a template will have configuration independent from each other. When a template extension is deleted or disabled through the setup app, all child cards created from that template will also be deleted or disabled, respectively. 58 | 59 | ### MarkdownTemplateConfig 60 | The template utilizies the customConfiguration prop to add its own configuration values to the dashboard's configuration step. When customConfiguration is set in a template, any cards produced from the template will be able to utilize the customConfiguration. Cards created from a template each can have their own unique values of customConfiguration. 61 | 62 | ## Sample page 63 | This page shows all the properties passed into the page. This is only for demonstration purposes. 64 | 65 | ## Utilities 66 | 67 | ### ReactIntlProviderWrapper.jsx 68 | Ellucian Experience has chosen to use 'react-intl' library for localizing the content displayed in the Ellucian Experience dashboard. Sample cards here have also used 'react-intl' library. To use 'react-intl' inside a card, you would typically initialize the IntlProvider and wrap your content with injectIntl. Instead of doing that, you can follow the patterns that these sample cards follow by using this ReactIntlProviderWrapper as a Higher-Order Component around your card. The localized strings are found inside .json files within ./src/i18n. 69 | If you choose to localize your cards, you can follow this pattern or use your preferred frameworks to manage localizations. 70 | 71 | ### Unit testing 72 | The project has jest and enzyme libraries present to run unit tests. By starting the test runner (npm run test), the project folders under src will be searched for files that end with test.js and execute those tests. 73 | -------------------------------------------------------------------------------- /sdk-samples/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/transform-runtime" 9 | ], 10 | "env": { 11 | "test": { 12 | "plugins": [ 13 | "rewire" 14 | ] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /sdk-samples/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | test/ -------------------------------------------------------------------------------- /sdk-samples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/errors", 6 | "plugin:import/warnings", 7 | "plugin:jsx-a11y/recommended", 8 | "plugin:react/recommended", 9 | "plugin:react-hooks/recommended", 10 | "plugin:jest/recommended" 11 | ], 12 | "parserOptions": { 13 | "babelOptions": { 14 | "presets": [ 15 | "@babel/preset-react" 16 | ] 17 | }, 18 | "ecmaFeatures": { 19 | "experimentalObjectRestSpread": true, 20 | "jsx": true 21 | }, 22 | "requireConfigFile": false, 23 | "sourceType": "module" 24 | }, 25 | "settings": { 26 | "import/resolver": { 27 | "node": { 28 | "extensions": [ 29 | ".js", 30 | ".jsx" 31 | ] 32 | } 33 | }, 34 | "react": { 35 | "version": "detect" 36 | }, 37 | "linkComponents": [ 38 | "Hyperlink", {"name": "Link", "linkAttribute": "to"} 39 | ] 40 | }, 41 | "env": { 42 | "browser": true, 43 | "node": true, 44 | "es6": true, 45 | "mocha": true, 46 | "jest/globals": true 47 | }, 48 | "rules": { 49 | "indent": [ 50 | "error", 51 | 4, 52 | { 53 | "MemberExpression": "off", 54 | "SwitchCase": 1 55 | } 56 | ], 57 | "linebreak-style": [ 58 | "error", 59 | "unix" 60 | ], 61 | "quotes": [ 62 | "error", 63 | "single" 64 | ], 65 | "semi": [ 66 | "error", 67 | "always" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sdk-samples/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | report.xml 4 | build/** 5 | .DS_Store 6 | *.log 7 | .env 8 | bak 9 | report.tap 10 | local 11 | .nyc_output/**/* 12 | coverage 13 | dist 14 | junit.xml 15 | server_report.xml 16 | client_report.xml 17 | tmp 18 | .vscode 19 | .eslintcache -------------------------------------------------------------------------------- /sdk-samples/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | report.xml 4 | build/** 5 | .DS_Store 6 | *.log 7 | .env 8 | bak 9 | report.tap 10 | local 11 | .nyc_output/**/* 12 | coverage 13 | dist 14 | junit.xml 15 | server_report.xml 16 | client_report.xml 17 | tmp 18 | .vscode 19 | -------------------------------------------------------------------------------- /sdk-samples/.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.1 -------------------------------------------------------------------------------- /sdk-samples/config/setupTests.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | global.React = React; 3 | global.contexts = {}; 4 | -------------------------------------------------------------------------------- /sdk-samples/extension.js: -------------------------------------------------------------------------------- 1 | const helper = require('./src/i18n/manifest.helper.js'); 2 | 3 | module.exports = { 4 | name: 'sdk-samples', 5 | publisher: 'Sample', 6 | configuration: { 7 | client: [{ 8 | key: 'extension-client-url', 9 | label: 'extension client url', 10 | type: 'url', 11 | required: true 12 | }, { 13 | 'key': 'extension-client-text', 14 | 'label': 'extension client text', 15 | 'type': 'text' 16 | }, { 17 | key: 'extension-client-password', 18 | label: 'extension client password', 19 | type: 'password' 20 | }], 21 | server: [{ 22 | key: 'extension-server-url', 23 | label: 'extension server url', 24 | type: 'url', 25 | required: true 26 | }, { 27 | key: 'extension-server-text', 28 | label: 'extension server text', 29 | type: 'text' 30 | }, { 31 | key: 'extension-server-password', 32 | label: 'extension server password', 33 | type: 'password' 34 | }] 35 | }, 36 | cards: [{ 37 | type: 'ThrowErrorCard', 38 | source: './src/cards/ThrowErrorCard.jsx', 39 | category: 'insights', 40 | title: 'Throw Error 2', 41 | displayCardType: 'Nothing but Error', 42 | description: 'Throws an Error' 43 | }, { 44 | type: 'CardConfigurationCard', 45 | source: './src/cards/CardConfigurationCard', 46 | title: 'Card Configuration', 47 | category: 'academics', 48 | displayCardType: 'Card Configuration', 49 | description: 'Card Configuration', 50 | configuration: { 51 | client: [{ 52 | key: 'card-client-url', 53 | label: 'card client url', 54 | type: 'url', 55 | required: true 56 | }, { 57 | key: 'card-client-text', 58 | label: 'card client text', 59 | type: 'text' 60 | }, { 61 | key: 'card-client-password', 62 | label: 'card client password', 63 | type: 'password' 64 | }], 65 | server: [{ 66 | key: 'card-server-url', 67 | label: 'card server url', 68 | type: 'url', 69 | required: true 70 | }, { 71 | key: 'card-server-text', 72 | label: 'card server text', 73 | type: 'text' 74 | }, { 75 | key: 'card-server-password', 76 | label: 'card server password', 77 | type: 'password' 78 | }] 79 | } 80 | }, { 81 | type: 'GraphQLQueryCard', 82 | source: './src/cards/GraphQLQueryCard', 83 | miniCardIcon: 'building', 84 | category: 'work', 85 | title: 'Buildings', 86 | displayCardType: 'GraphQL Query', 87 | description: 'GraphQL Query', 88 | queries: { 89 | 'list-sites': [ 90 | { 91 | resourceVersions: { 92 | sites: { min: 6 }, 93 | }, 94 | query: 95 | `{ 96 | sites: {sites} ( 97 | sort: { title: ASC } 98 | ) 99 | { 100 | edges { 101 | node { 102 | id 103 | title 104 | } 105 | } 106 | } 107 | }` 108 | } 109 | ], 110 | 'list-buildings': [ 111 | { 112 | resourceVersions: { 113 | buildings: { min: 6 }, 114 | sites: { min: 6 }, 115 | }, 116 | query: 117 | `query listBuildings($siteId: ID){ 118 | buildings : {buildings}( 119 | filter: { 120 | {site}: { 121 | id: { EQ: $siteId } 122 | } 123 | }, 124 | sort: { title: ASC } 125 | ) 126 | { 127 | edges { 128 | node { 129 | id 130 | title 131 | site : {site} { 132 | id 133 | } 134 | } 135 | } 136 | } 137 | }` 138 | } 139 | ] 140 | } 141 | }, { 142 | type: 'CacheCard', 143 | source: './src/cards/CacheCard', 144 | title: 'Cache Card', 145 | category: 'community', 146 | miniCardIcon: 'usd-circle', 147 | displayCardType: 'Cache Card', 148 | description: 'Cache Card' 149 | }, { 150 | type: 'PreventRemoveCard', 151 | source: './src/cards/PreventRemoveCard', 152 | category: 'myaccount', 153 | miniCardIcon: 'file-certificate', 154 | title: 'Prevent Remove', 155 | displayCardType: 'Prevent Remove', 156 | description: 'This card can prevent its removal' 157 | }, { 158 | type: 'DrilldownCard', 159 | source: './src/cards/DrilldownCard', 160 | miniCardIcon: 'list-view', 161 | title: 'Drilldown Example', 162 | displayCardType: 'Drilldown Example', 163 | description: 'This card demostrates drilldown pattern' 164 | }, { 165 | type: 'LoadingStateCard', 166 | source: './src/cards/LoadingStateCard', 167 | miniCardIcon: 'download', 168 | title: 'Loading State', 169 | displayCardType: 'Loading State', 170 | description: 'This card sets it state to loading for 10 seconds' 171 | }, { 172 | type: 'ErrorMessageCard', 173 | source: './src/cards/ErrorMessageCard', 174 | title: 'Error Message', 175 | displayCardType: 'Error Message', 176 | description: 'This card sets an error message to display' 177 | }, { 178 | type: 'PropsCard', 179 | source: './src/cards/PropsCard', 180 | title: { 181 | 'en-US': helper('en-US', 'Manifest-title'), 182 | 'en-AU': helper('en-AU', 'Manifest-title'), 183 | 'en-GB': helper('en-GB', 'Manifest-title'), 184 | 'fr-CA': helper('fr-CA', 'Manifest-title'), 185 | ar: helper('ar', 'Manifest-title'), 186 | es: helper('es', 'Manifest-title') 187 | }, 188 | displayCardType: { 189 | 'en-US': helper('en-US', 'Manifest-displayCardType'), 190 | 'en-AU': helper('en-AU', 'Manifest-displayCardType'), 191 | 'en-GB': helper('en-GB', 'Manifest-displayCardType'), 192 | 'fr-CA': helper('fr-CA', 'Manifest-displayCardType'), 193 | ar: helper('ar', 'Manifest-displayCardType'), 194 | es: helper('es', 'Manifest-displayCardType') 195 | }, 196 | description: { 197 | 'en-US': helper('en-US', 'Manifest-description'), 198 | 'en-AU': helper('en-AU', 'Manifest-description'), 199 | 'en-GB': helper('en-GB', 'Manifest-description'), 200 | 'fr-CA': helper('fr-CA', 'Manifest-description'), 201 | ar: helper('ar', 'Manifest-description'), 202 | es: helper('es', 'Manifest-description') 203 | }, 204 | pageRoute: { 205 | route: '/' 206 | } 207 | }, { 208 | type: 'MarkdownTemplate', 209 | source: './src/cards/MarkdownTemplate.jsx', 210 | title: 'Markdown Template', 211 | displayCardType: 'Markdown Template', 212 | description: 'Markdown Template', 213 | template: { 214 | icon: 'applications', 215 | title: 'Markdown Template', 216 | description: 'Markdown template description' 217 | }, 218 | customConfiguration: { 219 | source: './src/cards/MarkdownTemplateConfig.jsx' 220 | } 221 | }], 222 | page: { 223 | source: './src/page/index.jsx' 224 | } 225 | }; 226 | -------------------------------------------------------------------------------- /sdk-samples/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | collectCoverage: false, 4 | "collectCoverageFrom": [ 5 | "src/**/*.{js,jsx}", 6 | "!/src/utils/test-utils/enzymeSetup" 7 | ], 8 | coveragePathIgnorePatterns: [ 9 | "/node_modules" 10 | ], 11 | coverageDirectory: 'coverage', 12 | setupFilesAfterEnv: ['/config/setupTests.js'], 13 | setupFiles:[ 14 | 'raf/polyfill', 15 | '/src/utils/test-utils/enzymeSetup' 16 | ], 17 | coverageReporters: [ 18 | "lcov", "text", "cobertura" 19 | ], 20 | "reporters": [ 21 | "default", "jest-junit" 22 | ], 23 | transform: { 24 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest" 25 | }, 26 | "transformIgnorePatterns": [ "node_modules/(?!react-design-system|experience-extension)"], 27 | testPathIgnorePatterns: [ 28 | "/node_modules/", 29 | "/src/utils/test-utils/enzymeSetup" 30 | ], 31 | moduleNameMapper: { 32 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 33 | "\\.(css|scss)$": "identity-obj-proxy" 34 | }, 35 | moduleFileExtensions: [ 36 | "js", 37 | "jsx" 38 | ] 39 | }; 40 | -------------------------------------------------------------------------------- /sdk-samples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdk-samples", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/ellucian-developer/experience-sdk-sample-extensions" 6 | }, 7 | "version": "7.17.1", 8 | "description": "Description of sdk-samples", 9 | "main": "index.js", 10 | "license": "Apache 2.0", 11 | "scripts": { 12 | "lint": "npx eslint --ext .jsx,.js src", 13 | "build-dev": "webpack --progress --mode development --env verbose", 14 | "build-prod": "webpack --progress --mode production --env verbose", 15 | "deploy-dev": "webpack --progress --mode development --env verbose --env upload", 16 | "deploy-prod": "webpack --progress --mode production --env verbose --env upload", 17 | "watch-and-upload": "webpack --hot --watch --mode development --env verbose --env upload --env forceUpload", 18 | "start": "webpack serve --mode development --env verbose --env liveReload", 19 | "test": "cross-env BABEL_ENV=test JEST_JUNIT_OUTPUT=./report.xml jest --coverage" 20 | }, 21 | "dependencies": { 22 | "@ellucian/ds-icons": "https://cdn.elluciancloud.com/assets/EDS2/7.18.1/umd/path_design_system_icons.tgz", 23 | "@ellucian/experience-extension-utils": "https://cdn.elluciancloud.com/assets/SDK/utils/1.0.0/ellucian-experience-extension-utils-1.0.0.tgz", 24 | "@ellucian/react-design-system": "https://cdn.elluciancloud.com/assets/EDS2/7.18.1/umd/path_design_system.tgz", 25 | "classnames": "2.2.6", 26 | "date-fns": "2.29.3", 27 | "prop-types": "15.7.2", 28 | "react": "17.0.2", 29 | "react-dom": "17.0.2", 30 | "react-intl": "5.12.5", 31 | "react-markdown": "8.0.0", 32 | "react-router-dom": "5.2.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/eslint-parser": "7.17.0", 36 | "@babel/plugin-transform-runtime": "7.12.1", 37 | "@babel/preset-env": "7.12.1", 38 | "@babel/preset-react": "7.12.1", 39 | "@ellucian/experience-extension": "https://cdn.elluciancloud.com/assets/SDK/7.17.1/ellucian-experience-extension-7.17.1.tgz", 40 | "@wojtekmaj/enzyme-adapter-react-17": "0.6.7", 41 | "babel-plugin-rewire": "1.2.0", 42 | "cross-env": "7.0.2", 43 | "dotenv": "8.2.0", 44 | "enzyme": "3.11.0", 45 | "eslint": "8.8.0", 46 | "eslint-plugin-import": "2.25.4", 47 | "eslint-plugin-jest": "26.1.1", 48 | "eslint-plugin-jsx-a11y": "6.5.1", 49 | "eslint-plugin-react": "7.28.0", 50 | "eslint-plugin-react-hooks": "4.6.0", 51 | "expect": "26.6.2", 52 | "fs-extra": "9.0.1", 53 | "identity-obj-proxy": "3.0.0", 54 | "jest": "26.5.3", 55 | "jest-fetch-mock": "3.0.3", 56 | "jest-junit": "13.0.0", 57 | "jsonpath": "1.1.1", 58 | "rewiremock": "3.14.3", 59 | "webpack": "5.76.0", 60 | "webpack-cli": "4.10.0", 61 | "webpack-dev-server": "4.7.4" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sdk-samples/sample.env: -------------------------------------------------------------------------------- 1 | # Do not to commit this file to your source control system as it will contain sensitive secrets. If you use a CI/CD system such as Jenkins, use its mechanism for securely managing secrets and environment variables. 2 | # Follow the 'Quick Start' section in the readme.md 3 | EXPERIENCE_EXTENSION_UPLOAD_TOKEN= 4 | 5 | # The following are optional environment variables which can be used to setup your extension. 6 | # View 'Utilizing the Setup API' section of the readme.md for more details. 7 | # To utilize them, uncomment and fill out: 8 | # EXPERIENCE_EXTENSION_SHARED_SECRET= 9 | # EXPERIENCE_EXTENSION_ENABLED= 10 | # EXPERIENCE_EXTENSION_ENVIRONMENTS= -------------------------------------------------------------------------------- /sdk-samples/src/cards/CacheCard.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import PropTypes from 'prop-types'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { useIntl } from 'react-intl'; 6 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 7 | 8 | const styles = () => ({ 9 | count: { 10 | display: 'flex', 11 | flexDirection: 'column', 12 | alignItems: 'center', 13 | justifyContent: 'center', 14 | height: '100%' 15 | } 16 | }); 17 | 18 | const CACHE_KEY = 'local-cache-card:view-count'; 19 | const CACHE_SCOPE = 'local-cache-card:scope'; 20 | 21 | /** 22 | * Demonstrates the use of the SDK's `storeItem` function to cache data on browser local storage. 23 | * 24 | * @param {Object.} props Component props 25 | * @returns {React.Component} The Cache card 26 | */ 27 | const CacheCard = (props) => { 28 | const { classes, cache: { getItem, storeItem, removeItem }} = props; 29 | const intl = useIntl(); 30 | const [ viewedCount, setViewedCount ] = useState(0); 31 | 32 | /** 33 | * Resets the view count to zero. 34 | */ 35 | const resetCount = () => { 36 | setViewedCount(0); 37 | removeItem({ key: CACHE_KEY, scope: CACHE_SCOPE }); 38 | }; 39 | 40 | useEffect(() => { 41 | 42 | /** 43 | * Updates the cached view count 44 | */ 45 | const incrementCount = () => { 46 | const { data } = getItem({ key: CACHE_KEY, scope: CACHE_SCOPE }); 47 | const count = data ? data.count + 1 : 1; 48 | storeItem({ key: CACHE_KEY, scope: CACHE_SCOPE, data: { count } }); 49 | setViewedCount(count); 50 | }; 51 | 52 | // load and increment view count 53 | incrementCount(); 54 | 55 | // eslint-disable-next-line react-hooks/exhaustive-deps 56 | }, []); 57 | 58 | return ( 59 |
60 | 61 | {intl.formatMessage({ id: 'CacheCard-viewCount' }, { viewedCount })} 62 | 63 | 66 |
67 | ); 68 | }; 69 | 70 | CacheCard.propTypes = { 71 | classes: PropTypes.object.isRequired, 72 | cache: PropTypes.object.isRequired 73 | }; 74 | 75 | export default withIntl(withStyles(styles)(CacheCard)); 76 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/CardConfigurationCard.jsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import { spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 4 | import PropTypes from 'prop-types'; 5 | import React, { Fragment } from 'react'; 6 | 7 | const styles = () => ({ 8 | card: { 9 | marginRight: spacing40, 10 | marginLeft: spacing40, 11 | paddingBottom: spacing40 12 | } 13 | }); 14 | 15 | /** 16 | * Demonstrates the use of client configuration in a card. 17 | * 18 | * @param {Object.} props Component props 19 | * @returns {React.Component} The CardConfiguration card 20 | */ 21 | const CardConfigurationCard = (props) => { 22 | 23 | // get client configuration items 24 | const { classes, cardInfo: { configuration } } = props; 25 | 26 | const configurationItems = []; 27 | 28 | if (configuration) { 29 | Object.keys(configuration).forEach((key) => { 30 | if (typeof configuration[key] === 'string' || typeof configuration[key] === 'number') { 31 | configurationItems.push({ 32 | key, 33 | value: configuration[key] 34 | }); 35 | } 36 | }); 37 | } 38 | 39 | return ( 40 |
41 | {configurationItems.map((item) => ( 42 | 43 | 44 | {item.key}: 45 | 46 | 47 | {item.value} 48 | 49 | 50 | ))} 51 |
52 | ); 53 | }; 54 | 55 | CardConfigurationCard.propTypes = { 56 | classes: PropTypes.object.isRequired, 57 | cardInfo: PropTypes.object.isRequired 58 | }; 59 | 60 | export default withStyles(styles)(CardConfigurationCard); 61 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/DrilldownCard.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import { spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 4 | import PropTypes from 'prop-types'; 5 | import React, { useEffect, useState } from 'react'; 6 | import { useIntl } from 'react-intl'; 7 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 8 | 9 | const styles = { 10 | card: { 11 | marginRight: spacing40, 12 | marginLeft: spacing40 13 | } 14 | }; 15 | 16 | /** 17 | * Demonstrates the use of the `drillDown` function to navigate between views in a card. 18 | * 19 | * @param {Object.} props Component props 20 | * @returns {React.Component} The Drilldown card 21 | */ 22 | const DrilldownCard = (props) => { 23 | 24 | const { classes, cardControl: { drilldown, resetDrilldown } = {}} = props; 25 | const intl = useIntl(); 26 | const [ count, setCount ] = useState(0); 27 | const [ inDetail, setInDetail ] = useState(false); 28 | 29 | useEffect( 30 | () => { 31 | if (count > 0) { 32 | setInDetail(true); 33 | } 34 | }, 35 | [ count ] 36 | ); 37 | 38 | useEffect( 39 | () => { 40 | if (inDetail) { 41 | drilldown(() => { 42 | setInDetail(false); 43 | }, intl.formatMessage({ id: 'DrilldownCard-clicks' }, { count })); 44 | } 45 | }, 46 | // eslint-disable-next-line react-hooks/exhaustive-deps 47 | [ inDetail ] 48 | ); 49 | 50 | return inDetail ? ( 51 |
52 | 53 | {intl.formatMessage({ id: 'DrilldownCard-message' }, { count })} 54 | 55 | 63 |
64 | ) : ( 65 |
66 | 73 |
74 | ); 75 | }; 76 | 77 | DrilldownCard.propTypes = { 78 | classes: PropTypes.object.isRequired, 79 | cardControl: PropTypes.object.isRequired 80 | }; 81 | 82 | export default withIntl(withStyles(styles)(DrilldownCard)); 83 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/ErrorMessageCard.jsx: -------------------------------------------------------------------------------- 1 | import { Button, TextField, Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import { spacing20, spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 4 | import PropTypes from 'prop-types'; 5 | import React, { useState } from 'react'; 6 | import { useIntl } from 'react-intl'; 7 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 8 | 9 | const styles = () => ({ 10 | card: { 11 | marginRight: spacing40, 12 | marginLeft: spacing40, 13 | paddingBottom: spacing40 14 | }, 15 | errorMessageField: { 16 | marginTop: spacing20, 17 | marginBottom: spacing20 18 | } 19 | }); 20 | 21 | /** 22 | * Demonstrates the use of the SDK's {code}setErrorMessage{code} function to display a card-level 23 | * error message 24 | */ 25 | class ErrorMessageCard extends React.Component { 26 | render() { 27 | const { classes, cardControl: { setErrorMessage }, intl } = this.props; 28 | 29 | return ( 30 |
31 | 32 | {intl.formatMessage({ id: 'ErrorMessageCard-description' })} 33 | 34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | ErrorMessageCard.propTypes = { 41 | cardControl: PropTypes.object.isRequired, 42 | classes: PropTypes.object, 43 | intl: PropTypes.object 44 | }; 45 | 46 | export default withIntl(withStyles(styles)(ErrorMessageCard)); 47 | 48 | /** 49 | * Collects the attributes of the error message 50 | * 51 | * @param {Object.} props Component props 52 | * @returns {React.Component} The ErrorMessage card 53 | */ 54 | const ErrorMessage = (props) => { 55 | const [ headerMessage, setHeaderMessage ] = useState('Access denied'); 56 | const [ textMessage, setTextMessage ] = useState('You are not permitted to see this data'); 57 | const [ iconName, setIconName ] = useState('privacy'); 58 | const [ iconColor, setIconColor ] = useState('red'); 59 | const intl = useIntl(); 60 | const { classes } = props; 61 | 62 | /** 63 | * Set the appropriate message, based on which text field fired the change. 64 | * 65 | * @param {Event} event The change event 66 | */ 67 | const handleChange = (event) => { 68 | const { name, value } = event.target; 69 | 70 | switch (name) { 71 | case 'headerMessage': 72 | setHeaderMessage(value); 73 | break; 74 | case 'textMessage': 75 | setTextMessage(value); 76 | break; 77 | case 'iconName': 78 | setIconName(value); 79 | break; 80 | case 'iconColor': 81 | setIconColor(value); 82 | break; 83 | default: 84 | break; 85 | } 86 | }; 87 | 88 | /** 89 | * Show the card error message, using the provided values to configure it 90 | */ 91 | function submitValues() { 92 | const { setErrorMessage } = props; 93 | 94 | if (setErrorMessage != undefined) { 95 | setErrorMessage({ headerMessage, textMessage, iconName, iconColor }); 96 | } 97 | } 98 | 99 | return ( 100 |
101 | 111 | 121 | 131 | 141 | 149 |
150 | ); 151 | }; 152 | 153 | ErrorMessage.propTypes = { 154 | setErrorMessage: PropTypes.func.isRequired, 155 | classes: PropTypes.object 156 | }; 157 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/GraphQLQueryCard.jsx: -------------------------------------------------------------------------------- 1 | import { Dropdown, DropdownItem, List, ListItem, ListItemText, Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import { spacing10, spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 4 | import PropTypes from 'prop-types'; 5 | import React, { Fragment, useEffect, useState } from 'react'; 6 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 7 | import { useIntl } from 'react-intl'; 8 | 9 | const styles = () => ({ 10 | card: { 11 | marginRight: spacing40, 12 | marginLeft: spacing40, 13 | paddingTop: spacing10 14 | }, 15 | list: { 16 | paddingBottom: spacing40 17 | }, 18 | formControl: { 19 | marginTop: spacing10, 20 | marginBottom: spacing40 21 | }, 22 | text: { 23 | marginRight: spacing40, 24 | marginLeft: spacing40 25 | } 26 | }); 27 | 28 | const cacheKey = 'graphql-card:sites'; 29 | 30 | /** 31 | * Demonstrates how to use a GraphQL query to make an Ethos request. Uses the SDK's 32 | * {code}getEthosQuery{code} function 33 | * 34 | * It uses the "list-buildings" query defined in this extension's `extension.js` file. 35 | * 36 | * @param {Object.} props Component props 37 | * @returns {React.Component} The Props card 38 | */ 39 | const GraphQLQueryCard = (props) => { 40 | const { 41 | classes, 42 | cardControl: { 43 | setLoadingStatus, 44 | setErrorMessage 45 | }, 46 | data: { getEthosQuery }, 47 | cache: { getItem, storeItem } 48 | } = props; 49 | const intl = useIntl(); 50 | const [ buildings, setBuildings ] = useState(); 51 | const [ sites, setSites ] = useState(); 52 | const [ selectedSite, setSelectedSite ] = useState(); 53 | 54 | useEffect(() => { 55 | (async () => { 56 | setLoadingStatus(true); 57 | 58 | const {data: cachedData, expired: cachedDataExpired=true} = await getItem({key: cacheKey}); 59 | if (cachedData) { 60 | setLoadingStatus(false); 61 | setSites(() => cachedData); 62 | } 63 | if (cachedDataExpired || cachedData === undefined) { 64 | try { 65 | const sitesData = await getEthosQuery({ queryId: 'list-sites' }); 66 | const { data: { sites: { edges: siteEdges } = [] } = {} } = sitesData; 67 | const sites = siteEdges.map( edge => edge.node ); 68 | setSites(() => sites); 69 | storeItem({ key: cacheKey, data: sites }); 70 | setLoadingStatus(false); 71 | } catch (error) { 72 | console.error('ethosQuery failed', error); 73 | setErrorMessage({ 74 | headerMessage: intl.formatMessage({ id: 'GraphQLQueryCard-fetchFailed' }), 75 | textMessage: intl.formatMessage({ id: 'GraphQLQueryCard-sitesFetchFailed' }), 76 | iconName: 'error', 77 | iconColor: '#D42828' 78 | }); 79 | } 80 | } 81 | })(); 82 | // eslint-disable-next-line react-hooks/exhaustive-deps 83 | }, []); 84 | 85 | useEffect( 86 | () => { 87 | (async () => { 88 | if (selectedSite) { 89 | // load the buildings 90 | let buildings = []; 91 | 92 | try { 93 | const buildingsData = await getEthosQuery({ queryId: 'list-buildings', properties: {'siteId' : selectedSite } }); 94 | const { data: { buildings: { edges: buildingEdges } = [] } = {} } = buildingsData; 95 | buildings = buildingEdges.map( edge => edge.node ); 96 | } catch (error) { 97 | console.error('ethosQuery failed', error); 98 | setErrorMessage({ 99 | headerMessage: intl.formatMessage({ id: 'GraphQLQueryCard-fetchFailed' }), 100 | textMessage: intl.formatMessage({ id: 'GraphQLQueryCard-buildingsFetchFailed' }), 101 | iconName: 'error', 102 | iconColor: '#D42828' 103 | }); 104 | } 105 | setBuildings(() => buildings); 106 | } 107 | })(); 108 | }, 109 | // eslint-disable-next-line react-hooks/exhaustive-deps 110 | [ selectedSite ] 111 | ); 112 | 113 | /** 114 | * Handle a dropdown site selection, by loading the associated buildings 115 | * 116 | * @param {Event} event The dropdown selection event. 117 | */ 118 | const siteSelected = (event) => { 119 | setSelectedSite(event.target.value); 120 | }; 121 | 122 | return ( 123 | 124 | {sites && ( 125 |
126 | 135 | {sites.map((site) => { 136 | return ; 137 | })} 138 | 139 | {buildings && buildings.length > 0 && ( 140 | 141 | {buildings.map((building, index) => ( 142 | 143 | 144 | 145 | ))} 146 | 147 | )} 148 | {buildings && 149 | buildings.length == 0 && 150 | selectedSite && ( 151 | 152 | {intl.formatMessage({ id: 'GraphQLQueryCard-noBuildings' })} 153 | 154 | )} 155 |
156 | )} 157 | {sites && 158 | !selectedSite && ( 159 | 160 | {intl.formatMessage({ id: 'GraphQLQueryCard-noSelectedSite' })} 161 | 162 | )} 163 |
164 | ); 165 | }; 166 | 167 | GraphQLQueryCard.propTypes = { 168 | cardControl: PropTypes.object.isRequired, 169 | classes: PropTypes.object.isRequired, 170 | cache: PropTypes.object.isRequired, 171 | data: PropTypes.object.isRequired, 172 | mockSites: PropTypes.object, 173 | mockBuildings: PropTypes.object 174 | }; 175 | export default withIntl(withStyles(styles)(GraphQLQueryCard)); 176 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/LoadingStateCard.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import { spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 4 | import PropTypes from 'prop-types'; 5 | import React, { useState } from 'react'; 6 | import { useIntl } from 'react-intl'; 7 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 8 | 9 | const styles = () => ({ 10 | card: { 11 | marginRight: spacing40, 12 | marginLeft: spacing40 13 | } 14 | }); 15 | 16 | /** 17 | * Demonstrates how to put a card in the "loading" state, using he SDK's {code}setLoadingStatus{code} 18 | * function. 19 | */ 20 | class LoadingStateCard extends React.Component { 21 | render() { 22 | const { classes, cardControl: { setLoadingStatus }, intl } = this.props; 23 | 24 | return ( 25 |
26 | 27 | {intl.formatMessage({ id: 'LoadingStateCard-label' })} 28 | 29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | LoadingStateCard.propTypes = { 36 | cardControl: PropTypes.object.isRequired, 37 | classes: PropTypes.object.isRequired, 38 | intl: PropTypes.object 39 | }; 40 | 41 | export default withIntl(withStyles(styles)(LoadingStateCard)); 42 | 43 | /** 44 | * Renders a button that puts the card in a "loading" state for 10 seconds. 45 | * 46 | * @param {Object.} props Component properties 47 | * @returns {React.Component} A button that activates the loading state 48 | */ 49 | const LoadingButton = (props) => { 50 | const intl = useIntl(); 51 | const [ status, setStatus ] = useState('loaded'); 52 | 53 | /** 54 | * Put the card in "loading" mode 55 | */ 56 | function reload() { 57 | 58 | setStatus('loading'); 59 | const { setLoadingStatus } = props; 60 | 61 | // put the card in "loading" mode 62 | setLoadingStatus(true); 63 | 64 | // create an artificial delay 65 | setTimeout(reset, 10000); 66 | 67 | } 68 | 69 | /** 70 | * Disable the card's loading mode 71 | */ 72 | function reset() { 73 | 74 | setStatus('loaded'); 75 | 76 | const { setLoadingStatus } = props; 77 | 78 | setLoadingStatus(false); 79 | } 80 | 81 | return ( 82 |
83 | 84 | {intl.formatMessage({ id: 'LoadingStateCard-status' }, { status })} 85 | 86 | 89 |
90 | ); 91 | }; 92 | 93 | LoadingButton.propTypes = { 94 | setLoadingStatus: PropTypes.func.isRequired 95 | }; 96 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/MarkdownTemplate.jsx: -------------------------------------------------------------------------------- 1 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 2 | import { spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | import ReactMarkdown from 'react-markdown'; 6 | 7 | const styles = () => ({ 8 | card: { 9 | marginTop: 0, 10 | marginRight: spacing40, 11 | marginBottom: 0, 12 | marginLeft: spacing40 13 | } 14 | }); 15 | 16 | const MarkdownTemplate = (props) => { 17 | const { classes, cardInfo: { configuration: { customConfiguration = {} } } } = props; 18 | 19 | return ( 20 |
21 | {customConfiguration.markdown} 22 |
23 | ); 24 | }; 25 | 26 | MarkdownTemplate.propTypes = { 27 | classes: PropTypes.object.isRequired, 28 | cardInfo: PropTypes.object 29 | }; 30 | 31 | export default withStyles(styles)(MarkdownTemplate); 32 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/MarkdownTemplateConfig.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { TextField} from '@ellucian/react-design-system/core'; 4 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 5 | 6 | const MarkdownTemplateConfig = (props) => { 7 | const { 8 | cardControl: { 9 | setCustomConfiguration, 10 | setIsCustomConfigurationValid 11 | }, 12 | cardInfo: { 13 | configuration: { 14 | customConfiguration 15 | } 16 | } 17 | } = props; 18 | 19 | const validColors = ['orange', 'black', 'red', 'green', 'blue']; 20 | 21 | const client = customConfiguration ? customConfiguration.client : undefined; 22 | const [markdown, setMarkdown] = React.useState(client ? client.markdown : ''); 23 | const [color, setColor] = React.useState(client ? client.color : ''); 24 | const [markdownError, setMarkdownError] = React.useState(false); 25 | const [colorError, setColorError] = React.useState(false); 26 | 27 | 28 | // do an initial validation when the custom configuration first mounts; this is to catch 29 | // required-field errors, which wouldn't otherwise appear until the required fields are 30 | // interacted with 31 | useEffect(() => { 32 | updateCustomConfigVerification(); 33 | // eslint-disable-next-line react-hooks/exhaustive-deps 34 | }, []); 35 | 36 | // update the custom configuration whenever any values change 37 | useEffect(() => { 38 | updateCustomConfig(); 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, [markdown, color]); 41 | 42 | 43 | const handleCommentsChange = e => { 44 | const { value } = e.target; 45 | setMarkdown(value); 46 | }; 47 | 48 | const handleColorChange = e => { 49 | const { value } = e.target; 50 | setColor(value); 51 | }; 52 | 53 | const updateCustomConfig = () => { 54 | 55 | setCustomConfiguration({ 56 | customConfiguration: { 57 | client: { 58 | markdown: markdown, 59 | color: color 60 | } 61 | } 62 | }); 63 | 64 | }; 65 | 66 | // verify all custom config fields 67 | const updateCustomConfigVerification = () => { 68 | 69 | let errorCount = 0; 70 | let invalidColor = false; 71 | 72 | // markdown text is required 73 | if (markdown === undefined || markdown.length === 0) { 74 | errorCount++; 75 | } 76 | 77 | // if a color was entered, make sure it's valid 78 | if (color !== undefined && color.length > 0 && !validColors.includes(color)) { 79 | errorCount++; 80 | invalidColor = true; 81 | } 82 | 83 | // update individual field error states 84 | setMarkdownError(markdown === undefined || markdown.length === 0); 85 | setColorError(invalidColor); 86 | 87 | // register validation errors: whether there's an error, and how many errors there are 88 | setIsCustomConfigurationValid(errorCount === 0, errorCount); 89 | 90 | }; 91 | 92 | return ( 93 | 94 | 95 | 108 | 109 |
110 | 111 | 123 | 124 |
125 | ); 126 | }; 127 | 128 | MarkdownTemplateConfig.propTypes = { 129 | cardControl: PropTypes.object, 130 | cardInfo: PropTypes.object, 131 | intl: PropTypes.object 132 | }; 133 | 134 | export default withIntl(MarkdownTemplateConfig); -------------------------------------------------------------------------------- /sdk-samples/src/cards/PreventRemoveCard.jsx: -------------------------------------------------------------------------------- 1 | import { Switch, Typography } from '@ellucian/react-design-system/core'; 2 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 3 | import { spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 4 | import PropTypes from 'prop-types'; 5 | import React, { useState } from 'react'; 6 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 7 | 8 | const styles = () => ({ 9 | card: { 10 | marginRight: spacing40, 11 | marginLeft: spacing40 12 | } 13 | }); 14 | 15 | /** 16 | * Demonstrates how to prevent a card from being removed from the dashboard. Uses 17 | * the SDK's {code}setPreventRemove{code} and {code}setPreventRemoveMessage{code} 18 | * functions. 19 | */ 20 | class PreventRemoveCard extends React.Component { 21 | render() { 22 | const { classes, cardControl: { setPreventRemove, setPreventRemoveMessage }, intl } = this.props; 23 | 24 | return ( 25 |
26 | 27 | {intl.formatMessage({ id: 'PreventRemoveCard-switchLabel' })} 28 | 29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | PreventRemoveCard.propTypes = { 36 | cardControl: PropTypes.object.isRequired, 37 | classes: PropTypes.object.isRequired, 38 | intl: PropTypes.object 39 | }; 40 | 41 | export default withIntl(withStyles(styles)(PreventRemoveCard)); 42 | 43 | 44 | /** 45 | * A switch to enable/disable the ability for users to remove a card from the Experience dashboard. 46 | * 47 | * @param {Object.} props Component props 48 | * @returns {React.Component} A Switch control that enables/disables card removal 49 | */ 50 | const TogglePreventRemove = (props) => { 51 | const [ toggle, setToggle ] = useState(false); 52 | 53 | function toggleSwitch() { 54 | setToggle(!toggle); 55 | const { setPreventRemove, setPreventRemoveMessage } = props; 56 | 57 | if (setPreventRemove != undefined) { 58 | setPreventRemove(!toggle); 59 | setPreventRemoveMessage('You can\'t remove me!'); 60 | } 61 | } 62 | 63 | return ( 64 | 70 | ); 71 | }; 72 | 73 | TogglePreventRemove.propTypes = { 74 | setPreventRemove: PropTypes.func.isRequired, 75 | setPreventRemoveMessage: PropTypes.func.isRequired 76 | }; 77 | -------------------------------------------------------------------------------- /sdk-samples/src/cards/PropsCard.jsx: -------------------------------------------------------------------------------- 1 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 2 | import { spacing40 } from '@ellucian/react-design-system/core/styles/tokens'; 3 | import { Typography, TextLink, useDateLocale } from '@ellucian/react-design-system/core'; 4 | import PropTypes from 'prop-types'; 5 | import React from 'react'; 6 | import { 7 | useCache, 8 | useCardInfo, 9 | useData, 10 | useExperienceInfo, 11 | useExtensionControl, 12 | useExtensionInfo, 13 | useThemeInfo, 14 | useUserInfo, 15 | useDashboardInfo, 16 | useCardControl, 17 | usePageControl, 18 | usePageInfo 19 | } from '@ellucian/experience-extension-utils'; 20 | import format from 'date-fns/format'; 21 | 22 | const styles = () => ({ 23 | card: { 24 | marginTop: 0, 25 | marginRight: spacing40, 26 | marginBottom: 0, 27 | marginLeft: spacing40 28 | } 29 | }); 30 | 31 | /** 32 | * Demonstrates how to access all of the data and functions provided to extension cards, 33 | * via both props and hooks. 34 | * 35 | * @param {Object.} props Component properties 36 | * @returns {React.Component} The Props card 37 | */ 38 | const PropsCard = (props) => { 39 | const { classes } = props; 40 | // get a locale object for use with the format function, below. 41 | const localeContext = useDateLocale(); 42 | return ( 43 |
44 | 45 | {format(new Date(), 'PPPPp', {locale: localeContext})} 46 | 47 | 48 | Properties 49 | 50 |
{JSON.stringify(props, undefined, 3)}
51 | 52 | Hooks 53 | 54 |
 useCache {JSON.stringify(useCache(), undefined, 3)}
55 |
 useCardInfo {JSON.stringify(useCardInfo(), undefined, 3)}
56 |
 useData {JSON.stringify(useData(), undefined, 3)}
57 |
 useExperienceInfo {JSON.stringify(useExperienceInfo(), undefined, 3)}
58 |
 useExtensionControl {JSON.stringify(useExtensionControl(), undefined, 3)}
59 |
 useExtensionInfo {JSON.stringify(useExtensionInfo(), undefined, 3)}
60 |
 useThemeInfo {JSON.stringify(useThemeInfo(), undefined, 3)}
61 |
 useUserInfo {JSON.stringify(useUserInfo(), undefined, 3)}
62 |
 useDashboardInfo {JSON.stringify(useDashboardInfo(), undefined, 3)}
63 |
 useCardControl {JSON.stringify(useCardControl(), undefined, 3)}
64 |
 usePageControl {JSON.stringify(usePageControl(), undefined, 3)}
65 |
 usePageInfo {JSON.stringify(usePageInfo(), undefined, 3)}
66 | 67 | For more information regarding hooks and props, visit the 68 | 69 | Props and Hooks 70 | 71 | section of the Ellucian Experience SDK documentation. 72 | 73 |
74 | ); 75 | }; 76 | 77 | PropsCard.propTypes = { 78 | classes: PropTypes.object.isRequired 79 | }; 80 | 81 | export default withStyles(styles)(PropsCard); -------------------------------------------------------------------------------- /sdk-samples/src/cards/ThrowErrorCard.jsx: -------------------------------------------------------------------------------- 1 | import { ButtonGroup, Button } from '@ellucian/react-design-system/core'; 2 | import PropTypes from 'prop-types'; 3 | import React, { useState } from 'react'; 4 | import { useIntl } from 'react-intl'; 5 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 6 | 7 | const ThrowErrorComponent = () => { 8 | // This error is not caught by our extension code, 9 | // but Experience will catch it and display a generic error 10 | throw new Error('Nothing but error'); 11 | }; 12 | 13 | /** 14 | * A card that demonstrates how to handle both caught and uncaught exceptions. Uses the 15 | * SDK {code}setErrorMessage{code} function. 16 | * 17 | * @param {Object.} props Component props 18 | * @returns {React.Component} The ThrowError card 19 | */ 20 | const ThrowErrorCard = (props) => { 21 | const { cardControl: { setErrorMessage }} = props; 22 | const [dashboardError, setDashboardError] = useState(false); 23 | const intl = useIntl(); 24 | 25 | /** 26 | * Induces an exception, and then catches and handles it gracefully within the card. 27 | */ 28 | const throwExtensionError = () => { 29 | try { 30 | console.log(window.accessing.undefinedVariable); 31 | } catch (error) { 32 | 33 | // use the SDK to post an error message in the card 34 | setErrorMessage({ 35 | headerMessage: intl.formatMessage({ id: 'ThrowErrorCard-errorHeaderMessage' }), 36 | textMessage: intl.formatMessage({ id: 'ThrowErrorCard-errorTextMessage' }), 37 | iconName: 'error', 38 | iconColor: '#D42828' 39 | }); 40 | 41 | } 42 | }; 43 | 44 | return ( 45 | 46 | {dashboardError ? ( 47 | 48 | ) : ( 49 | 50 | 53 | 56 | 57 | )} 58 | 59 | ); 60 | }; 61 | 62 | ThrowErrorCard.propTypes = { 63 | cardControl: PropTypes.object 64 | }; 65 | 66 | export default withIntl(ThrowErrorCard); 67 | -------------------------------------------------------------------------------- /sdk-samples/src/i18n/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "CacheCard-reset": "إعادة الضبط", 4 | "CacheCard-viewCount": "عدد مرات العرض: {viewedCount}", 5 | "DrilldownCard-clickMe": "انقر هنا", 6 | "DrilldownCard-clicks": "عدد النقرات: {count}", 7 | "DrilldownCard-goBack": "العودة", 8 | "DrilldownCard-message": "لقد قمت بالنقر {count} مرة/مرات", 9 | "ErrorMessageCard-buttonAria": "ضبط رسالة الخطأ", 10 | "ErrorMessageCard-description": "ضبط رسالة خطأ للعرض", 11 | "ErrorMessageCard-headerMessage": "رسالة العنوان", 12 | "ErrorMessageCard-iconColor": "لون الأيقونة", 13 | "ErrorMessageCard-iconName": "اسم الأيقونة", 14 | "ErrorMessageCard-textMessage": "رسالة نصية", 15 | "ErrorMessageCard-submit": "إرسال", 16 | "GraphQLQueryCard-noBuildings": "لا توجد مبانٍ لعرضها", 17 | "GraphQLQueryCard-noSelectedSite": "تحديد موقع لعرض المباني ذات الصلة", 18 | "GraphQLQueryCard-sites": "المواقع", 19 | "GraphQLQueryCard-fetchFailed": "فشل الإحضار", 20 | "GraphQLQueryCard-sitesFetchFailed": "تعذر إحضار المواقع", 21 | "GraphQLQueryCard-buildingsFetchFailed": "تعذر إحضار المباني", 22 | "LoadingStateCard-label": "ضبط حالة التحميل لمدة 10 ثوانٍ", 23 | "LoadingStateCard-reload": "إعادة التحميل", 24 | "LoadingStateCard-status": "الحالة: {status}", 25 | "MarkdownTemplate-markdown-placeholder": "أدخل نص Markdown", 26 | "MarkdownTemplate-color-placeholder": "اختر اللون", 27 | "MarkdownTemplate-valid-colors": "الألوان الصالحة: {colors}", 28 | "Manifest-description": "وصف props", 29 | "Manifest-displayCardType": "عنوان عرض props", 30 | "Manifest-title": "عنوان props", 31 | "Page-title": "عنوان props و hooks", 32 | "PreventRemoveCard-switchLabel": "تبديل القدرة على إزالة بطاقة:", 33 | "ThrowErrorCard-errorHeaderMessage": "حدث خطأ متوقع", 34 | "ThrowErrorCard-errorTextMessage": "تم اكتشاف هذا الخطأ بواسطة كود الامتداد وتم استدعاء setErrorMessage عن عمد", 35 | "ThrowErrorCard-extensionErrorButtonText": "طرح خطأ الامتداد", 36 | "ThrowErrorCard-dashboardErrorButtonText": "طرح خطأ لوحة المعلومات" 37 | } 38 | } -------------------------------------------------------------------------------- /sdk-samples/src/i18n/en-AU.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "CacheCard-reset": "Reset", 4 | "CacheCard-viewCount": "View Count: {viewedCount}", 5 | "DrilldownCard-clickMe": "Click me", 6 | "DrilldownCard-clicks": "Clicks: {count}", 7 | "DrilldownCard-goBack": "Go back", 8 | "DrilldownCard-message": "You clicked {count} times", 9 | "ErrorMessageCard-buttonAria": "Set error message", 10 | "ErrorMessageCard-description": "Set an error message to display", 11 | "ErrorMessageCard-headerMessage": "Header message", 12 | "ErrorMessageCard-iconColor": "Icon colour", 13 | "ErrorMessageCard-iconName": "Icon name", 14 | "ErrorMessageCard-textMessage": "Text message", 15 | "ErrorMessageCard-submit": "Submit", 16 | "GraphQLQueryCard-noBuildings": "No buildings to show", 17 | "GraphQLQueryCard-noSelectedSite": "Select a site to show related buildings", 18 | "GraphQLQueryCard-sites": "Sites", 19 | "GraphQLQueryCard-fetchFailed": "Fetch failed", 20 | "GraphQLQueryCard-sitesFetchFailed": "Could not fetch sites", 21 | "GraphQLQueryCard-buildingsFetchFailed": "Could not fetch buildings", 22 | "LoadingStateCard-label": "Set the loading status for 10 seconds", 23 | "LoadingStateCard-reload": "Reload", 24 | "LoadingStateCard-status": "Status {status}", 25 | "MarkdownTemplate-markdown-placeholder": "Enter Markdown text", 26 | "MarkdownTemplate-color-placeholder": "Choose your colour", 27 | "MarkdownTemplate-valid-colors": "Valid colours: {colors}", 28 | "Manifest-description": "Props Description", 29 | "Manifest-displayCardType": "Props Display Title", 30 | "Manifest-title": "Props Title", 31 | "Page-title": "Props and Hooks Title", 32 | "PreventRemoveCard-switchLabel": "Toggle ability to remove card:", 33 | "ThrowErrorCard-errorHeaderMessage": "An expected error occurred", 34 | "ThrowErrorCard-errorTextMessage": "This error was caught by the extension code and setErrorMessage was intentionally called", 35 | "ThrowErrorCard-extensionErrorButtonText": "Throw Extension Error", 36 | "ThrowErrorCard-dashboardErrorButtonText": "Throw Dashboard Error" 37 | } 38 | } -------------------------------------------------------------------------------- /sdk-samples/src/i18n/en-GB.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "CacheCard-reset": "Reset", 4 | "CacheCard-viewCount": "View Count: {viewedCount}", 5 | "DrilldownCard-clickMe": "Click me", 6 | "DrilldownCard-clicks": "Clicks: {count}", 7 | "DrilldownCard-goBack": "Go back", 8 | "DrilldownCard-message": "You clicked {count} times", 9 | "ErrorMessageCard-buttonAria": "Set error message", 10 | "ErrorMessageCard-description": "Set an error message to display", 11 | "ErrorMessageCard-headerMessage": "Header message", 12 | "ErrorMessageCard-iconColor": "Icon colour", 13 | "ErrorMessageCard-iconName": "Icon name", 14 | "ErrorMessageCard-textMessage": "Text message", 15 | "ErrorMessageCard-submit": "Submit", 16 | "GraphQLQueryCard-noBuildings": "No buildings to show", 17 | "GraphQLQueryCard-noSelectedSite": "Select a site to show related buildings", 18 | "GraphQLQueryCard-sites": "Sites", 19 | "GraphQLQueryCard-fetchFailed": "Fetch failed", 20 | "GraphQLQueryCard-sitesFetchFailed": "Could not fetch sites", 21 | "GraphQLQueryCard-buildingsFetchFailed": "Could not fetch buildings", 22 | "LoadingStateCard-label": "Set the loading status for 10 seconds", 23 | "LoadingStateCard-reload": "Reload", 24 | "LoadingStateCard-status": "Status: {status}", 25 | "MarkdownTemplate-markdown-placeholder": "Enter Markdown text", 26 | "MarkdownTemplate-color-placeholder": "Choose your colour", 27 | "MarkdownTemplate-valid-colors": "Valid colours: {colors}", 28 | "Manifest-description": "Props Description", 29 | "Manifest-displayCardType": "Props Display Title", 30 | "Manifest-title": "Props Title", 31 | "Page-title": "Props and Hooks Title", 32 | "PreventRemoveCard-switchLabel": "Toggle ability to remove card:", 33 | "ThrowErrorCard-errorHeaderMessage": "An expected error occurred", 34 | "ThrowErrorCard-errorTextMessage": "This error was caught by the extension code and setErrorMessage was intentionally called", 35 | "ThrowErrorCard-extensionErrorButtonText": "Throw Extension Error", 36 | "ThrowErrorCard-dashboardErrorButtonText": "Throw Dashboard Error" 37 | } 38 | } -------------------------------------------------------------------------------- /sdk-samples/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "CacheCard-reset": "Reset", 4 | "CacheCard-viewCount": "View Count: {viewedCount}", 5 | "DrilldownCard-clickMe": "Click me", 6 | "DrilldownCard-clicks": "Clicks: {count}", 7 | "DrilldownCard-goBack": "Go back", 8 | "DrilldownCard-message": "You clicked {count} times", 9 | "ErrorMessageCard-buttonAria": "Set error message", 10 | "ErrorMessageCard-description": "Set an error message to display", 11 | "ErrorMessageCard-headerMessage": "Header message", 12 | "ErrorMessageCard-iconColor": "Icon color", 13 | "ErrorMessageCard-iconName": "Icon name", 14 | "ErrorMessageCard-textMessage": "Text message", 15 | "ErrorMessageCard-submit": "Submit", 16 | "GraphQLQueryCard-noBuildings": "No buildings to show", 17 | "GraphQLQueryCard-noSelectedSite": "Select a site to show related buildings", 18 | "GraphQLQueryCard-sites": "Sites", 19 | "GraphQLQueryCard-fetchFailed": "Fetch failed", 20 | "GraphQLQueryCard-sitesFetchFailed": "Could not fetch sites", 21 | "GraphQLQueryCard-buildingsFetchFailed": "Could not fetch buildings", 22 | "LoadingStateCard-label": "Set the loading status for 10 seconds", 23 | "LoadingStateCard-reload": "Reload", 24 | "LoadingStateCard-status": "Status: {status}", 25 | "MarkdownTemplate-markdown-placeholder": "Enter Markdown text", 26 | "MarkdownTemplate-color-placeholder": "Choose your color", 27 | "MarkdownTemplate-valid-colors": "Valid colors: {colors}", 28 | "Manifest-description": "Props Description", 29 | "Manifest-displayCardType": "Props Display Title", 30 | "Manifest-title": "Props Title", 31 | "Page-title": "Props and Hooks Title", 32 | "PreventRemoveCard-switchLabel": "Toggle ability to remove card:", 33 | "ThrowErrorCard-errorHeaderMessage": "An expected error occurred", 34 | "ThrowErrorCard-errorTextMessage": "This error was caught by the extension code and setErrorMessage was intentionally called", 35 | "ThrowErrorCard-extensionErrorButtonText": "Throw Extension Error", 36 | "ThrowErrorCard-dashboardErrorButtonText": "Throw Dashboard Error" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sdk-samples/src/i18n/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "CacheCard-reset": "Restablecer", 4 | "CacheCard-viewCount": "Conteo de vistos: {viewedCount}", 5 | "DrilldownCard-clickMe": "Haga clic aquí", 6 | "DrilldownCard-clicks": "Número de clics: {count}", 7 | "DrilldownCard-goBack": "Regresar", 8 | "DrilldownCard-message": "Hizo clic {count} veces", 9 | "ErrorMessageCard-buttonAria": "Definir el mensaje de error", 10 | "ErrorMessageCard-description": "Defina el mensaje de error a desplegar", 11 | "ErrorMessageCard-headerMessage": "Mensaje del encabezado", 12 | "ErrorMessageCard-iconColor": "Color del icono", 13 | "ErrorMessageCard-iconName": "Nombre del icono", 14 | "ErrorMessageCard-textMessage": "Mensaje de texto", 15 | "ErrorMessageCard-submit": "Enviar", 16 | "GraphQLQueryCard-noBuildings": "No hay edificios para mostrar", 17 | "GraphQLQueryCard-noSelectedSite": "Seleccione un sitio para mostrar los edificios relacionados", 18 | "GraphQLQueryCard-sites": "Sitios", 19 | "GraphQLQueryCard-fetchFailed": "Hubo un error al obtener los datos", 20 | "GraphQLQueryCard-sitesFetchFailed": "No pudo obtener los sitios", 21 | "GraphQLQueryCard-buildingsFetchFailed": "No pudo obtener los edificios", 22 | "LoadingStateCard-label": "Establezca el estatus de carga por 10 segundos", 23 | "LoadingStateCard-reload": "Recargar", 24 | "LoadingStateCard-status": "Estatus: {status}", 25 | "MarkdownTemplate-markdown-placeholder": "Ingrese el texto de Markdown", 26 | "MarkdownTemplate-color-placeholder": "Elija un color", 27 | "MarkdownTemplate-valid-colors": "Colores válidos: {colors}", 28 | "Manifest-description": "Descripción de props", 29 | "Manifest-displayCardType": "Título de visualización de props", 30 | "Manifest-title": "Título de props", 31 | "Page-title": "Título de props y hooks", 32 | "PreventRemoveCard-switchLabel": "Active la capacidad para eliminar la tarjeta:", 33 | "ThrowErrorCard-errorHeaderMessage": "Ocurrió un error esperado", 34 | "ThrowErrorCard-errorTextMessage": "Este error fue detectado por el código de la extensión y se llamó intencionalmente a setErrorMessage", 35 | "ThrowErrorCard-extensionErrorButtonText": "Iniciar el error de extensión", 36 | "ThrowErrorCard-dashboardErrorButtonText": "Iniciar el error de tablero" 37 | } 38 | } -------------------------------------------------------------------------------- /sdk-samples/src/i18n/fr-CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "messages": { 3 | "CacheCard-reset": "Réinitialiser", 4 | "CacheCard-viewCount": "Nombre de vues : {viewedCount}", 5 | "DrilldownCard-clickMe": "Cliquez-moi", 6 | "DrilldownCard-clicks": "Clics : {count}", 7 | "DrilldownCard-goBack": "Retour", 8 | "DrilldownCard-message": "Vous avez cliqué {count} fois", 9 | "ErrorMessageCard-buttonAria": "Configurer un message d’erreur", 10 | "ErrorMessageCard-description": "Configurer un message d’erreur à afficher", 11 | "ErrorMessageCard-headerMessage": "Message d’en-tête", 12 | "ErrorMessageCard-iconColor": "Couleur de l’icône", 13 | "ErrorMessageCard-iconName": "Nom de l’icône", 14 | "ErrorMessageCard-textMessage": "Message texte", 15 | "ErrorMessageCard-submit": "Soumettre", 16 | "GraphQLQueryCard-noBuildings": "Aucun bâtiment à afficher", 17 | "GraphQLQueryCard-noSelectedSite": "Sélectionnez un site pour afficher les bâtiments associés", 18 | "GraphQLQueryCard-sites": "Sites", 19 | "GraphQLQueryCard-fetchFailed": "Échec de la récupération", 20 | "GraphQLQueryCard-sitesFetchFailed": "Impossible de récupérer les sites", 21 | "GraphQLQueryCard-buildingsFetchFailed": "Impossible de récupérer les bâtiments", 22 | "LoadingStateCard-label": "Définir le statut de chargement pendant 10 secondes", 23 | "LoadingStateCard-reload": "Recharger", 24 | "LoadingStateCard-status": "Statut : {status}", 25 | "MarkdownTemplate-markdown-placeholder": "Saisir le texte Markdown", 26 | "MarkdownTemplate-color-placeholder": "Choisir la couleur", 27 | "MarkdownTemplate-valid-colors": "Couleurs valides : {colors}", 28 | "Manifest-description": "Description des propriétés", 29 | "Manifest-displayCardType": "Titre de l’affichage des propriétés", 30 | "Manifest-title": "Titre des propriétés", 31 | "Page-title": "Titre des propriétés et des Hooks", 32 | "PreventRemoveCard-switchLabel": "Activer la capacité pour supprimer la carte :", 33 | "ThrowErrorCard-errorHeaderMessage": "Une erreur attendue s’est produite", 34 | "ThrowErrorCard-errorTextMessage": "Cette erreur a été détectée par le code d’extension et setErrorMessage a été déclenché intentionnellement.", 35 | "ThrowErrorCard-extensionErrorButtonText": "Lancer une erreur d’extension", 36 | "ThrowErrorCard-dashboardErrorButtonText": "Lancer une erreur de tableau de bord" 37 | } 38 | } -------------------------------------------------------------------------------- /sdk-samples/src/i18n/intlUtility.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | import ENGLISH_TRANSLATION from '../i18n/en.json'; 3 | 4 | export const getMessages = (userLocale) => { 5 | const {messages: baseMessages } = ENGLISH_TRANSLATION; 6 | 7 | try { 8 | const { messages: localeMessages } = require(`../i18n/${userLocale}.json`); 9 | // check for territory specific translations 10 | if (localeMessages) { 11 | return Object.assign({}, baseMessages, localeMessages); 12 | } else { 13 | // check for language translations 14 | const actionLanguage = userLocale.split(/[-_]/)[0]; 15 | const { messages: localeMessages } = require(`../i18n/${actionLanguage}.json`); 16 | return Object.assign({}, baseMessages, localeMessages); 17 | } 18 | } catch (e) { 19 | try { 20 | const actionLanguage = userLocale.split(/[-_]/)[0]; 21 | const { messages: localeMessages } = require(`../i18n/${actionLanguage}.json`); 22 | return Object.assign({}, baseMessages, localeMessages); 23 | } catch (e) { 24 | // This userLocale is not supported. 25 | return baseMessages; 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /sdk-samples/src/i18n/manifest.helper.js: -------------------------------------------------------------------------------- 1 | const ar = require('./ar.json').messages; 2 | const enAu = require('./en-AU.json').messages; 3 | const enGb = require('./en-GB.json').messages; 4 | const enUs = require('./en.json').messages; 5 | const es = require('./es.json').messages; 6 | const frCa = require('./fr-CA.json').messages; 7 | 8 | const locales = { 9 | ar, 10 | 'en-AU': enAu, 11 | 'en-GB': enGb, 12 | 'en-US': enUs, 13 | es, 14 | 'fr-CA': frCa 15 | }; 16 | 17 | /** 18 | * @param {String} locale a locale key which follows BCP 47 format 19 | * @param {String} property the property key to be translated 20 | * @returns a translation based on the locale and property 21 | */ 22 | function i18nHelper(locale, property) { 23 | return locales[locale][property]; 24 | } 25 | 26 | module.exports = i18nHelper; -------------------------------------------------------------------------------- /sdk-samples/src/page/index.jsx: -------------------------------------------------------------------------------- 1 | import { withStyles } from '@ellucian/react-design-system/core/styles'; 2 | import { spacing20 } from '@ellucian/react-design-system/core/styles/tokens'; 3 | import { Typography, TextLink } from '@ellucian/react-design-system/core'; 4 | import PropTypes from 'prop-types'; 5 | import React from 'react'; 6 | import { 7 | useCache, 8 | useCardInfo, 9 | useData, 10 | useExperienceInfo, 11 | useExtensionControl, 12 | useExtensionInfo, 13 | useThemeInfo, 14 | useUserInfo, 15 | useDashboardInfo, 16 | useCardControl, 17 | usePageControl, 18 | usePageInfo 19 | } from '@ellucian/experience-extension-utils'; 20 | import { useIntl } from 'react-intl'; 21 | import { withIntl } from '../utils/ReactIntlProviderWrapper'; 22 | 23 | const styles = () => ({ 24 | card: { 25 | margin: `0 ${spacing20}` 26 | } 27 | }); 28 | 29 | /** 30 | * Demonstrates how to access all of the data and functions provided to extension cards, 31 | * via both props and hooks. 32 | * 33 | * This page is invoked from the Props card. 34 | * 35 | * @param {Object.} props Page props 36 | * @returns {React.Component} The Props card 37 | */ 38 | const PropsPage = (props) => { 39 | const { classes } = props; 40 | const { setPageTitle } = usePageControl(); 41 | const intl = useIntl(); 42 | 43 | setPageTitle(intl.formatMessage({ id: 'Page-title' })); 44 | 45 | return ( 46 |
47 | 48 | Properties 49 | 50 |
{JSON.stringify(props, undefined, 3)}
51 | 52 | Hooks 53 | 54 |
 useCache {JSON.stringify(useCache(), undefined, 3)}
55 |
 useCardInfo {JSON.stringify(useCardInfo(), undefined, 3)}
56 |
 useData {JSON.stringify(useData(), undefined, 3)}
57 |
 useExperienceInfo {JSON.stringify(useExperienceInfo(), undefined, 3)}
58 |
 useExtensionControl {JSON.stringify(useExtensionControl(), undefined, 3)}
59 |
 useExtensionInfo {JSON.stringify(useExtensionInfo(), undefined, 3)}
60 |
 useThemeInfo {JSON.stringify(useThemeInfo(), undefined, 3)}
61 |
 useUserInfo {JSON.stringify(useUserInfo(), undefined, 3)}
62 |
 useDashboardInfo {JSON.stringify(useDashboardInfo(), undefined, 3)}
63 |
 useCardControl {JSON.stringify(useCardControl(), undefined, 3)}
64 |
 usePageControl {JSON.stringify(usePageControl(), undefined, 3)}
65 |
 usePageInfo {JSON.stringify(usePageInfo(), undefined, 3)}
66 | 67 | For more information regarding hooks and props, visit the 68 | 69 | Props and Hooks 70 | 71 | section of the Ellucian Experience SDK documentation. 72 | 73 |
74 | ); 75 | }; 76 | 77 | PropsPage.propTypes = { 78 | classes: PropTypes.object.isRequired 79 | }; 80 | 81 | export default withIntl(withStyles(styles)(PropsPage)); -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/CacheCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CacheCard from '../../cards/CacheCard'; 3 | import { mountWithExtensionProps } from '../../utils/test-utils/enzymeUtil'; 4 | 5 | describe(' { 6 | it('Pulls data from cache', () => { 7 | 8 | const mockCacheFunctions = { 9 | getItem: () => ({}), 10 | removeItem: jest.fn(), 11 | storeItem: jest.fn() 12 | }; 13 | 14 | const card = mountWithExtensionProps(); 15 | expect(card.text().includes('View Count: 1')).toBe(true); 16 | }); 17 | }); -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/CardConfigurationCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CardConfigurationCard from '../../cards/CardConfigurationCard'; 3 | import { mountWithExtensionProps } from '../../utils/test-utils/enzymeUtil'; 4 | 5 | describe('', () => { 6 | it('Loads with configuration values', () => { 7 | 8 | const mockCardInfo = { 9 | configuration: { 10 | "mockLabel": 'mock-url-label', 11 | "mockUrl": 'https://mockurl' 12 | } 13 | }; 14 | 15 | const card = mountWithExtensionProps(); 16 | expect(card.prop('cardInfo').configuration.mockLabel).toEqual('mock-url-label'); 17 | expect(card.prop('cardInfo').configuration.mockUrl).toEqual('https://mockurl'); 18 | }); 19 | }); -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/ErrorMessageCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ErrorMessageCard from '../../cards/ErrorMessageCard'; 3 | import { mountWithExtensionProps } from '../../utils/test-utils/enzymeUtil'; 4 | import { shallow } from 'enzyme'; 5 | 6 | describe('', () => { 7 | it('Loads with error', () => { 8 | 9 | const card = mountWithExtensionProps(); 10 | card.find('button').simulate('click'); 11 | expect(() => shallow()).toThrowError(); 12 | }); 13 | }); -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/LoadingStateCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoadingStateCard from '../../cards/LoadingStateCard'; 3 | import { mountWithExtensionProps } from '../../utils/test-utils/enzymeUtil'; 4 | 5 | describe('', () => { 6 | it('Sets the card to loading', () => { 7 | 8 | const mockCardControl = { 9 | setLoadingStatus: jest.fn() 10 | }; 11 | 12 | const card = mountWithExtensionProps(); 13 | const loadingButton = card.find('button'); 14 | loadingButton.simulate('click'); 15 | expect(card.text().includes('Status: loading')).toBe(true); 16 | }); 17 | }); -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/MarkdownTemplate.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MarkdownTemplate from '../../cards/MarkdownTemplate'; 3 | import { mountWithExtensionProps } from '../../utils/test-utils/enzymeUtil'; 4 | 5 | jest.mock("react-markdown", () => (props) => { 6 | return <>{props.children} 7 | }) 8 | 9 | describe('', () => { 10 | it('Sets markdown text', () => { 11 | 12 | const mockCardInfo = { 13 | configuration: { 14 | customConfiguration: { 15 | markdown: '### some text' 16 | } 17 | } 18 | }; 19 | 20 | const card = mountWithExtensionProps(); 21 | expect(card.text().includes('some text')).toBe(true); 22 | }); 23 | }); -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/PropsCard.test.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import PropsCard from '../../cards/PropsCard'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('', () => { 6 | it('Loads props', () => { 7 | 8 | const ContextProvider = React.createContext({}); 9 | 10 | const wrapper = shallow( 11 | 12 | 13 | 14 | ) 15 | 16 | expect(wrapper.find('pre').exists()); 17 | }) 18 | }) -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/PropsPage.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropsPage from '../../page/index'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('', () => { 6 | it('Loads props page', () => { 7 | 8 | const ContextProvider = React.createContext({}); 9 | 10 | const wrapper = shallow( 11 | 12 | 13 | 14 | ) 15 | 16 | expect(wrapper.find('pre').exists()); 17 | }) 18 | }) -------------------------------------------------------------------------------- /sdk-samples/src/test/cards/ThrowErrorCard.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ThrowErrorCard from '../../cards/ThrowErrorCard'; 3 | import { shallow } from 'enzyme'; 4 | 5 | describe('', () => { 6 | it('Throws an error', () => { 7 | expect(() => shallow()).toThrowError(); 8 | }); 9 | }); -------------------------------------------------------------------------------- /sdk-samples/src/test/helpers/manifest.helper.test.js: -------------------------------------------------------------------------------- 1 | import i18nHelper from "../../i18n/manifest.helper"; 2 | 3 | describe('validates i18nHelper', () => { 4 | it('validates with en-US', () => { 5 | const enUSTranslation = i18nHelper('en-US', 'Manifest-title'); 6 | 7 | expect(enUSTranslation).toBe('Props Title'); 8 | }); 9 | }) -------------------------------------------------------------------------------- /sdk-samples/src/utils/ReactIntlProviderWrapper.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { injectIntl, IntlProvider } from 'react-intl'; 3 | import PropTypes from 'prop-types'; 4 | import { getMessages } from '../i18n/intlUtility'; 5 | 6 | /** 7 | * An HOC (higher-order component) that injects react-intl internationalization resources into the given component. 8 | * 9 | * Works in concert with react-intl's {code}useIntl{code} hook, which the wrapped components can use 10 | * to retrieve internationalization properties. 11 | * 12 | * @param {React.Component} Component The component into which we're injecting internationalization properties. 13 | * @returns {React.Component} Wrapped component 14 | */ 15 | export function withIntl(Component) { 16 | let InjectedComponent; 17 | 18 | class WithIntl extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | InjectedComponent = injectIntl(Component); 22 | } 23 | render() { 24 | const { userInfo: { locale } } = this.props; 25 | 26 | return ( 27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | WithIntl.propTypes = { 34 | userInfo: PropTypes.object 35 | }; 36 | WithIntl.displayName = `WithIntl(${Component.displayName})`; 37 | return WithIntl; 38 | } 39 | -------------------------------------------------------------------------------- /sdk-samples/src/utils/test-utils/enzymeSetup.js: -------------------------------------------------------------------------------- 1 | var enzyme = require('enzyme'); 2 | var Adapter = require('@wojtekmaj/enzyme-adapter-react-17'); 3 | 4 | // polyfill window.crypto for testing nanoid 5 | var nodeCrypto = require('crypto'); 6 | 7 | // mocks the standard browser crypto function in the global environment; this is necessary for 8 | // running SDK unit tests, as the Path component library depends on the presence of crypto 9 | global.crypto = { 10 | // eslint-disable-next-line no-sync 11 | getRandomValues: function(buffer) { return nodeCrypto.randomFillSync(buffer);} 12 | }; 13 | 14 | enzyme.configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /sdk-samples/src/utils/test-utils/enzymeUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper for enzyme's mount that will wrap the test element with Path design system and react-intl 3 | */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import { IntlProvider } from 'react-intl'; 7 | import { mount as m } from 'enzyme'; 8 | import { getMessages } from '../../i18n/intlUtility'; 9 | import { EDSApplication } from '@ellucian/react-design-system/core'; 10 | 11 | const locale = 'en'; 12 | const mockSetErrorMessage = jest.fn(); 13 | 14 | /** 15 | * Wrapper around enzyme's mount to add necessary parents and to add additional needed props 16 | * @param {JSXElement} component - The JSX element to wrap with needed parents and pass extension props 17 | * @return {WrappedComponent} wrapped component 18 | */ 19 | export function mount(component) { 20 | return m(component, { 21 | wrappingComponent: CardWrappingComponent 22 | }); 23 | } 24 | 25 | /** 26 | * Wrapper around enzyme's mount to add necessary parents 27 | * @param {JSXElement} component - The JSX element to wrap with needed parents 28 | * @return {WrappedComponent} wrapped component 29 | */ 30 | export function mountWithExtensionProps(component) { 31 | return m(withProps(component), { 32 | wrappingComponent: CardWrappingComponent 33 | }); 34 | } 35 | 36 | function withProps(element) { 37 | const oldProps = element.props; 38 | const tempElement= React.cloneElement( 39 | element, 40 | { 41 | userInfo: { locale }, 42 | cardControl: { setErrorMessage: mockSetErrorMessage } 43 | } 44 | ); 45 | return React.cloneElement(tempElement, oldProps); 46 | } 47 | 48 | function CardWrappingComponent(props) { 49 | const { children } = props; 50 | return ( 51 | 52 | 53 | {children} 54 | 55 | 56 | ); 57 | } 58 | 59 | CardWrappingComponent.propTypes = { 60 | children: PropTypes.node 61 | }; 62 | 63 | CardWrappingComponent.defaultProps = { 64 | children: null 65 | }; 66 | -------------------------------------------------------------------------------- /sdk-samples/webpack.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const packageJson = require('./package.json'); 3 | const extensionConfig = require('./extension.js'); 4 | 5 | const { webpackConfigBuilder } = require('@ellucian/experience-extension'); 6 | 7 | module.exports = async (env, options) => { 8 | 9 | // Generate Webpack configuration based on the extension.js file 10 | // and any optional env flags ("--env verbose", "--env upload", etc) 11 | const webpackConfig = await webpackConfigBuilder({ 12 | extensionConfig: extensionConfig, 13 | extensionVersion: packageJson.version, 14 | mode: options.mode || 'production', 15 | verbose: env.verbose || process.env.EXPERIENCE_EXTENSION_VERBOSE || false, 16 | upload: env.upload || process.env.EXPERIENCE_EXTENSION_UPLOAD || false, 17 | forceUpload: env.forceUpload || process.env.EXPERIENCE_EXTENSION_FORCE_UPLOAD || false, 18 | uploadToken: process.env.EXPERIENCE_EXTENSION_UPLOAD_TOKEN, 19 | liveReload: env.liveReload || false, 20 | port: process.env.PORT || 8082 21 | }); 22 | 23 | // For advanced scenarios, dynamically modify webpackConfig here. 24 | 25 | return webpackConfig; 26 | }; --------------------------------------------------------------------------------