diff --git a/package-lock.json b/package-lock.json index 1b356ecfd..b5a8a6af1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@fortawesome/free-brands-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/frontend-plugin-framework": "^1.3.0", "@openedx/paragon": "^22.1.1", "@optimizely/react-sdk": "^2.9.1", "@redux-devtools/extension": "3.3.0", @@ -31,6 +32,7 @@ "query-string": "7.1.3", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-error-boundary": "^4.0.13", "react-helmet": "6.1.0", "react-loading-skeleton": "3.4.0", "react-redux": "7.2.9", @@ -3395,6 +3397,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@openedx/frontend-plugin-framework": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.3.0.tgz", + "integrity": "sha512-qLtX/4HIuWXiIhBdtBuL6mPVbV2un0rsFYx3I5+3tIUf7+T7WRq81a6JHU5QGyAmZy9dfiv7QwbqwiEQOVXVuQ==", + "dependencies": { + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", + "classnames": "^2.3.2", + "core-js": "3.37.1", + "react-redux": "7.2.9", + "redux": "4.2.1", + "regenerator-runtime": "0.14.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "^7.0.0 || ^8.0.0", + "@openedx/paragon": "^21.0.0 || ^22.0.0", + "prop-types": "^15.8.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-error-boundary": "^4.0.11" + } + }, + "node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/@openedx/paragon": { "version": "22.7.0", "license": "Apache-2.0", @@ -4053,6 +4086,21 @@ } } }, + "node_modules/@testing-library/react-hooks/node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "license": "MIT", @@ -13216,15 +13264,12 @@ } }, "node_modules/react-error-boundary": { - "version": "3.1.4", - "license": "MIT", + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", "dependencies": { "@babel/runtime": "^7.12.5" }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, "peerDependencies": { "react": ">=16.13.1" } diff --git a/package.json b/package.json index 7d3966bb4..f344c8d88 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@fortawesome/free-brands-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/frontend-plugin-framework": "^1.3.0", "@openedx/paragon": "^22.1.1", "@optimizely/react-sdk": "^2.9.1", "@redux-devtools/extension": "3.3.0", @@ -54,6 +55,7 @@ "query-string": "7.1.3", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-error-boundary": "^4.0.13", "react-helmet": "6.1.0", "react-loading-skeleton": "3.4.0", "react-redux": "7.2.9", diff --git a/src/MainApp.jsx b/src/MainApp.jsx index 26c2cf594..b107e365a 100755 --- a/src/MainApp.jsx +++ b/src/MainApp.jsx @@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet'; import { Navigate, Route, Routes } from 'react-router-dom'; import { - EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk, + EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, } from './common-components'; import configureStore from './data/configureStore'; import { @@ -22,6 +22,7 @@ import { import { updatePathWithQueryParams } from './data/utils'; import { ForgotPasswordPage } from './forgot-password'; import Logistration from './logistration/Logistration'; +import MainAppSlot from './plugin-slots/MainAppSlot'; import { ProgressiveProfiling } from './progressive-profiling'; import { RecommendationsPage } from './recommendations'; import { RegistrationPage } from './register'; @@ -36,7 +37,6 @@ const MainApp = () => ( - {getConfig().ZENDESK_KEY && } } /> ( } /> } /> + ); diff --git a/src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx b/src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx new file mode 100644 index 000000000..f5262d81e --- /dev/null +++ b/src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx @@ -0,0 +1,29 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { render } from '@testing-library/react'; + +import MainAppSlot from './index'; + +jest.mock('@openedx/frontend-plugin-framework', () => ({ + PluginSlot: jest.fn(() => null), +})); + +describe('MainAppSlot', () => { + it('renders without crashing', () => { + render(); + }); + + it('renders a PluginSlot component', () => { + render(); + expect(PluginSlot).toHaveBeenCalled(); + }); + + it('passes the correct id prop to PluginSlot', () => { + render(); + expect(PluginSlot).toHaveBeenCalledWith({ id: 'main_app_slot' }, {}); + }); + + it('does not render any children', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); +}); diff --git a/src/plugin-slots/MainAppSlot/README.md b/src/plugin-slots/MainAppSlot/README.md new file mode 100644 index 000000000..ff755fc0a --- /dev/null +++ b/src/plugin-slots/MainAppSlot/README.md @@ -0,0 +1,41 @@ +# Main App Slot + +### Slot ID: `main_app_slot` + +## Description + +This slot is used for adding content at the root level. + +## Example + +The following `env.config.jsx` will render a component at the MFE root level. + +![Screenshot of Content added after the Main App Slot](./images/main_app_slot.png) + +```js +import { + DIRECT_PLUGIN, + PLUGIN_OPERATIONS, +} from "@openedx/frontend-plugin-framework"; +import { ExampleComponent } from "@openedx/frontend-plugin-example"; + +const config = { + pluginSlots: { + main_app_slot: { + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: "example-component", + type: DIRECT_PLUGIN, + priority: 60, + RenderWidget: ExampleComponent, + }, + }, + ], + }, + }, +}; + +export default config; +``` diff --git a/src/plugin-slots/MainAppSlot/images/main_app_slot.png b/src/plugin-slots/MainAppSlot/images/main_app_slot.png new file mode 100644 index 000000000..bc842adec Binary files /dev/null and b/src/plugin-slots/MainAppSlot/images/main_app_slot.png differ diff --git a/src/plugin-slots/MainAppSlot/index.jsx b/src/plugin-slots/MainAppSlot/index.jsx new file mode 100644 index 000000000..6c869a258 --- /dev/null +++ b/src/plugin-slots/MainAppSlot/index.jsx @@ -0,0 +1,7 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; + +const MainAppSlot = () => ( + +); + +export default MainAppSlot; diff --git a/src/plugin-slots/README.md b/src/plugin-slots/README.md new file mode 100644 index 000000000..4971bb382 --- /dev/null +++ b/src/plugin-slots/README.md @@ -0,0 +1,3 @@ +# `frontend-app-authn` Plugin Slots + +- [`main_app_slot`](./MainAppSlot/)