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/)