diff --git a/package-lock.json b/package-lock.json
index 49bad4e72..66c0217b5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,9 +10,9 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
- "@edx/frontend-component-footer": "12.3.0",
+ "@edx/frontend-component-footer": "12.4.0",
"@edx/frontend-component-header": "4.7.1",
- "@edx/frontend-platform": "5.4.0",
+ "@edx/frontend-platform": "5.6.1",
"@edx/paragon": "^20.44.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -21,7 +21,7 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@pact-foundation/pact": "^11.0.2",
"classnames": "2.3.2",
- "core-js": "3.32.2",
+ "core-js": "3.33.0",
"history": "5.3.0",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
@@ -30,6 +30,7 @@
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
+ "react-error-boundary": "^4.0.11",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.16.0",
@@ -44,8 +45,8 @@
"universal-cookie": "4.0.4"
},
"devDependencies": {
- "@commitlint/cli": "17.7.2",
- "@commitlint/config-angular": "17.7.0",
+ "@commitlint/cli": "17.8.0",
+ "@commitlint/config-angular": "17.8.0",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.9.17",
"@edx/reactifex": "2.2.0",
@@ -127,16 +128,81 @@
}
},
"node_modules/@babel/code-frame": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz",
- "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==",
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
"dependencies": {
- "@babel/highlight": "^7.22.5"
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
},
"engines": {
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/compat-data": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz",
@@ -208,11 +274,11 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz",
- "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dependencies": {
- "@babel/types": "^7.22.5",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -348,20 +414,20 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
@@ -493,9 +559,9 @@
}
},
"node_modules/@babel/helper-split-export-declaration": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz",
- "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==",
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
"dependencies": {
"@babel/types": "^7.22.5"
},
@@ -512,9 +578,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
- "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
@@ -555,12 +621,12 @@
}
},
"node_modules/@babel/highlight": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
- "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.22.5",
- "chalk": "^2.0.0",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
"js-tokens": "^4.0.0"
},
"engines": {
@@ -632,9 +698,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
- "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -1978,31 +2044,31 @@
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/@babel/template": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz",
- "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==",
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
"dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz",
- "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==",
- "dependencies": {
- "@babel/code-frame": "^7.22.5",
- "@babel/generator": "^7.22.5",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
- "@babel/helper-split-export-declaration": "^7.22.5",
- "@babel/parser": "^7.22.5",
- "@babel/types": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -2011,12 +2077,12 @@
}
},
"node_modules/@babel/types": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz",
- "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -2044,14 +2110,14 @@
}
},
"node_modules/@commitlint/cli": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.7.2.tgz",
- "integrity": "sha512-t3N7TZq7lOeqTOyEgfGcaltHqEJf7YDlPg75MldeVPPyz14jZq/+mbGF9tueDLFX8R6RwdymrN6D+U5XwZ8Iwg==",
+ "version": "17.8.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-17.8.0.tgz",
+ "integrity": "sha512-D3LdyZYbiRyAChfJMNlAd9f2P9vNQ7GWbI9gN2o7L5hF07QJDqj4z/pcJF3PjDbJWOaUUXla287RdDmmKqH2WQ==",
"dev": true,
"dependencies": {
"@commitlint/format": "^17.4.4",
- "@commitlint/lint": "^17.7.0",
- "@commitlint/load": "^17.7.2",
+ "@commitlint/lint": "^17.8.0",
+ "@commitlint/load": "^17.8.0",
"@commitlint/read": "^17.5.1",
"@commitlint/types": "^17.4.4",
"execa": "^5.0.0",
@@ -2068,9 +2134,9 @@
}
},
"node_modules/@commitlint/config-angular": {
- "version": "17.7.0",
- "resolved": "https://registry.npmjs.org/@commitlint/config-angular/-/config-angular-17.7.0.tgz",
- "integrity": "sha512-ZWLgVw4se/vmNHfWiti3OIx295KgyCgZ5LjDeLFbHQe9WXMX3lhwnaeJkum4smsQhQaa3/JvufKzxV/3LDGMJA==",
+ "version": "17.8.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/config-angular/-/config-angular-17.8.0.tgz",
+ "integrity": "sha512-3Jygi+j6AqbVbzZujQZqQhk1WPF4ipeh8MHcXlHa1humQQ2ROqfWUqdFlAxn9iIE93lbJz/ANGthJ9+k9sG7Zg==",
"dev": true,
"dependencies": {
"@commitlint/config-angular-type-enum": "^17.4.0"
@@ -2141,9 +2207,9 @@
}
},
"node_modules/@commitlint/is-ignored": {
- "version": "17.7.0",
- "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.7.0.tgz",
- "integrity": "sha512-043rA7m45tyEfW7Zv2vZHF++176MLHH9h70fnPoYlB1slKBeKl8BwNIlnPg4xBdRBVNPaCqvXxWswx2GR4c9Hw==",
+ "version": "17.8.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-17.8.0.tgz",
+ "integrity": "sha512-8bR6rxNcWaNprPBdE4ePIOwbxutTQGOsRPYWssX+zjGxnEljzaZSGzFUOMxapYILlf8Tts/O1wPQgG549Rdvdg==",
"dev": true,
"dependencies": {
"@commitlint/types": "^17.4.4",
@@ -2154,12 +2220,12 @@
}
},
"node_modules/@commitlint/lint": {
- "version": "17.7.0",
- "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.7.0.tgz",
- "integrity": "sha512-TCQihm7/uszA5z1Ux1vw+Nf3yHTgicus/+9HiUQk+kRSQawByxZNESeQoX9ujfVd3r4Sa+3fn0JQAguG4xvvbA==",
+ "version": "17.8.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-17.8.0.tgz",
+ "integrity": "sha512-4ihwnqOY4TcJN6iz5Jv1LeYavvBllONwFyGxOIWmCT5s4PNMb43cws2TUdbXTZL1Vq59etGKd5LWYDFPVbs5EA==",
"dev": true,
"dependencies": {
- "@commitlint/is-ignored": "^17.7.0",
+ "@commitlint/is-ignored": "^17.8.0",
"@commitlint/parse": "^17.7.0",
"@commitlint/rules": "^17.7.0",
"@commitlint/types": "^17.4.4"
@@ -2169,9 +2235,9 @@
}
},
"node_modules/@commitlint/load": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.7.2.tgz",
- "integrity": "sha512-XA7WTnsjHZ4YH6ZYsrnxgLdXzriwMMq+utZUET6spbOEEIPBCDLdOQXS26P+v3TTO4hUHOEhzUquaBv3jbBixw==",
+ "version": "17.8.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-17.8.0.tgz",
+ "integrity": "sha512-9VnGXYJCP4tXmR4YrwP8n5oX6T5ZsHfPQq6WuUQOvAI+QsDQMaTGgTRXr7us+xsjz+b+mMBSagogqfUx2aixyw==",
"dev": true,
"dependencies": {
"@commitlint/config-validator": "^17.6.7",
@@ -2514,9 +2580,9 @@
}
},
"node_modules/@edx/frontend-component-footer": {
- "version": "12.3.0",
- "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.3.0.tgz",
- "integrity": "sha512-ivCtioyP4SceYM4/ugVtif4c41Y+epA0NM7sSB/x6s9A/RTQXb2TY3fDc9lB3ah/0+pRwGVJJEVYkPAZ4JdC/g==",
+ "version": "12.4.0",
+ "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-12.4.0.tgz",
+ "integrity": "sha512-DmAC8eTB4ARYlzBewzxWB3Afn9v8j9nSudqDd6LiXHUYDZ8Z7QPrMfmrmfDHllflVd22NvljmP2hp+WdtjkgMA==",
"dependencies": {
"@edx/paragon": "^21.3.1",
"@fortawesome/fontawesome-svg-core": "6.4.2",
@@ -2534,9 +2600,9 @@
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@edx/paragon": {
- "version": "21.3.1",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.3.1.tgz",
- "integrity": "sha512-bXTUaOEmT8XLnDQzYS8QLMvWK5K2BN4jHlx25lO8N0XWRQeDiQTdbx8OrEbv8QOPTlrv0an5MZc+qjlleJFObg==",
+ "version": "21.5.3",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-21.5.3.tgz",
+ "integrity": "sha512-Xyq7gWLb4n8qWMsNBIPV3tZnOK0bBdHyqj8UVOrFbpBCSPlOES06qpcpT4IrtpfCh0LmUCBiIk5j9XMz3p6zaw==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -2855,9 +2921,9 @@
}
},
"node_modules/@edx/frontend-platform": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.4.0.tgz",
- "integrity": "sha512-cz9yQfHJk1PMQdhxeyIXXiBNqaG9dQZpcBgodmVlLnL/PeN1CuRVjjW98WlKYSrxoZAH5wdgUOr0hKRW3OyBAA==",
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.6.1.tgz",
+ "integrity": "sha512-7MOIjGGYplVY7yHrSea90EkQ24UxKxRKU9FaihB41yUSL/Vin1txDuIn3059Xr+60QfIKRsym+LogXe9IZ47Dw==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.1.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -2944,9 +3010,9 @@
}
},
"node_modules/@edx/paragon": {
- "version": "20.46.2",
- "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.2.tgz",
- "integrity": "sha512-px+KS/BV1CbiMKgfVgUofyjJi4CHUCUOLRukJbT66VPPqWP4Xon5Rns6uohoratPXMg2kNN46v2L8wIwqKQ4Lw==",
+ "version": "20.46.3",
+ "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.46.3.tgz",
+ "integrity": "sha512-cHxoxoOREVFbBqW9IRAtlIAQo1lcF9JJXkLoEw1Vam6oetKSa5Mc0SL5kykbV+1iRPP7kS8A0Csf5nRr0oolLQ==",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.1.18",
@@ -2981,21 +3047,21 @@
}
},
"node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-common-types": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz",
- "integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==",
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz",
+ "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/paragon/node_modules/@fortawesome/fontawesome-svg-core": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz",
- "integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==",
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz",
+ "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==",
"hasInstallScript": true,
"dependencies": {
- "@fortawesome/fontawesome-common-types": "6.4.0"
+ "@fortawesome/fontawesome-common-types": "6.4.2"
},
"engines": {
"node": ">=6"
@@ -5006,11 +5072,11 @@
}
},
"node_modules/@restart/hooks": {
- "version": "0.4.9",
- "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz",
- "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==",
+ "version": "0.4.11",
+ "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.11.tgz",
+ "integrity": "sha512-Ft/ncTULZN6ldGHiF/k5qt72O8JyRMOeg0tApvCni8LkoiEahO+z3TNxfXIVGy890YtWVDvJAl662dVJSJXvMw==",
"dependencies": {
- "dequal": "^2.0.2"
+ "dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
@@ -5644,9 +5710,9 @@
}
},
"node_modules/@types/react-transition-group": {
- "version": "4.4.6",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
- "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
+ "version": "4.4.7",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz",
+ "integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==",
"dependencies": {
"@types/react": "*"
}
@@ -5710,9 +5776,9 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
"node_modules/@types/warning": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
- "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA=="
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.1.tgz",
+ "integrity": "sha512-ywJmriP+nvjBKNBEMaNZgj2irZHoxcKeYcyMLbqhYKbDVn8yCIULy2Ol/tvIb37O3IBeZj3RU4tXqQTtGwoAMg=="
},
"node_modules/@types/ws": {
"version": "8.5.5",
@@ -7279,9 +7345,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001546",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz",
- "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==",
+ "version": "1.0.30001549",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz",
+ "integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==",
"funding": [
{
"type": "opencollective",
@@ -7921,9 +7987,9 @@
}
},
"node_modules/core-js": {
- "version": "3.32.2",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz",
- "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==",
+ "version": "3.33.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.0.tgz",
+ "integrity": "sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@@ -10292,6 +10358,14 @@
"node": ">=8"
}
},
+ "node_modules/filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@@ -14563,9 +14637,9 @@
}
},
"node_modules/jquery": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
- "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"peer": true
},
"node_modules/js-base64": {
@@ -15084,39 +15158,6 @@
"node": ">= 12"
}
},
- "node_modules/mailto-link/node_modules/filter-obj": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
- "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/mailto-link/node_modules/query-string": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz",
- "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==",
- "dependencies": {
- "decode-uri-component": "^0.2.0",
- "filter-obj": "^1.1.0",
- "split-on-first": "^1.0.0",
- "strict-uri-encode": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/mailto-link/node_modules/split-on-first": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
- "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -15791,9 +15832,9 @@
}
},
"node_modules/object-code": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.2.4.tgz",
- "integrity": "sha512-uGq4ETUuWe+GA586NXEriiaozNuff+YNFXlpD8cVrM1GoiuTZpCABP+bZCWDrvQDoCiSTyiWAFHD/HF/iwhb2w=="
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.0.tgz",
+ "integrity": "sha512-PLplgvzuFhSPBuTX/mtaXEnU3c6g7qKflVVQbV9VWEnV/34iKeAX1jeDNCKq1OgGlsnkA/NjldCzTbHxa7Wj4A=="
},
"node_modules/object-copy": {
"version": "0.1.0",
@@ -17508,6 +17549,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/query-string": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz",
+ "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==",
+ "dependencies": {
+ "decode-uri-component": "^0.2.0",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -17825,6 +17883,17 @@
"react": ">= 16.8 || 18.0.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
+ "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-error-overlay": {
"version": "6.0.11",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
@@ -17836,9 +17905,9 @@
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-focus-lock": {
- "version": "2.9.4",
- "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.4.tgz",
- "integrity": "sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg==",
+ "version": "2.9.5",
+ "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.5.tgz",
+ "integrity": "sha512-h6vrdgUbsH2HeD5I7I3Cx1PPrmwGuKYICS+kB9m+32X/9xHRrAbxgvaBpG7BFBN9h3tO+C3qX1QAVESmi4CiIA==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"focus-lock": "^0.11.6",
@@ -17858,12 +17927,12 @@
}
},
"node_modules/react-focus-on": {
- "version": "3.8.1",
- "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.8.1.tgz",
- "integrity": "sha512-fQcBx+SZMgXoRL+69r5+ic4bdVgqaCeKeoFPra8yhcSuL/3unWavfdirEFBGgH71K+RiocMTS6DETHcX0SlOZg==",
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/react-focus-on/-/react-focus-on-3.9.1.tgz",
+ "integrity": "sha512-IYo2j4mgNpZEJNv+/XzZs3S3xhJbR+AFop092h4OMW7sbFpIMVWxp/Z61V/gfpsgOi7VnoSFXP2bfOWWkjjtOw==",
"dependencies": {
"aria-hidden": "^1.2.2",
- "react-focus-lock": "^2.9.2",
+ "react-focus-lock": "^2.9.4",
"react-remove-scroll": "^2.5.6",
"react-style-singleton": "^2.2.0",
"tslib": "^2.3.1",
@@ -20116,6 +20185,14 @@
"wbuf": "^1.7.3"
}
},
+ "node_modules/split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -21563,9 +21640,13 @@
}
},
"node_modules/uuid": {
- "version": "9.0.0",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
- "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
"bin": {
"uuid": "dist/bin/uuid"
}
diff --git a/package.json b/package.json
index 7632ea1c6..0e747b58e 100644
--- a/package.json
+++ b/package.json
@@ -29,9 +29,9 @@
],
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
- "@edx/frontend-component-footer": "12.3.0",
+ "@edx/frontend-component-footer": "12.4.0",
"@edx/frontend-component-header": "4.7.1",
- "@edx/frontend-platform": "5.4.0",
+ "@edx/frontend-platform": "5.6.1",
"@edx/paragon": "^20.44.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
@@ -40,7 +40,7 @@
"@fortawesome/react-fontawesome": "0.2.0",
"@pact-foundation/pact": "^11.0.2",
"classnames": "2.3.2",
- "core-js": "3.32.2",
+ "core-js": "3.33.0",
"history": "5.3.0",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
@@ -49,6 +49,7 @@
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
+ "react-error-boundary": "^4.0.11",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.16.0",
@@ -63,8 +64,8 @@
"universal-cookie": "4.0.4"
},
"devDependencies": {
- "@commitlint/cli": "17.7.2",
- "@commitlint/config-angular": "17.7.0",
+ "@commitlint/cli": "17.8.0",
+ "@commitlint/config-angular": "17.8.0",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.9.17",
"@edx/reactifex": "2.2.0",
diff --git a/plugins/Plugin.jsx b/plugins/Plugin.jsx
new file mode 100644
index 000000000..f68f109c4
--- /dev/null
+++ b/plugins/Plugin.jsx
@@ -0,0 +1,93 @@
+'use client';
+
+import React, {
+ useEffect, useMemo, useState,
+} from 'react';
+import PropTypes from 'prop-types';
+import { ErrorBoundary } from 'react-error-boundary';
+import { logError } from '@edx/frontend-platform/logging';
+import {
+ dispatchMountedEvent, dispatchReadyEvent, dispatchUnmountedEvent, useHostEvent,
+} from './data/hooks';
+import { PLUGIN_RESIZE } from './data/constants';
+
+// see example-plugin-app/src/PluginOne.jsx for example of customizing errorFallback
+function errorFallbackDefault() {
+ return (
+
+
+ Oops! An error occurred. Please refresh the screen to try again.
+
+
+ );
+}
+
+// eslint-disable-next-line react/function-component-definition
+export default function Plugin({
+ children, className, style, ready, errorFallbackProp,
+}) {
+ const [dimensions, setDimensions] = useState({
+ width: null,
+ height: null,
+ });
+
+ const finalStyle = useMemo(() => ({
+ ...dimensions,
+ ...style,
+ }), [dimensions, style]);
+
+ const errorFallback = errorFallbackProp || errorFallbackDefault;
+
+ // Error logging function
+ // Need to confirm: When an error is caught here, the logging will be sent to the child MFE's logging service
+ const logErrorToService = (error, info) => {
+ logError(error, { stack: info.componentStack });
+ };
+
+ useHostEvent(PLUGIN_RESIZE, ({ payload }) => {
+ setDimensions({
+ width: payload.width,
+ height: payload.height,
+ });
+ });
+
+ useEffect(() => {
+ dispatchMountedEvent();
+
+ return () => {
+ dispatchUnmountedEvent();
+ };
+ }, []);
+
+ useEffect(() => {
+ if (ready) {
+ dispatchReadyEvent();
+ }
+ }, [ready]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+Plugin.propTypes = {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ errorFallbackProp: PropTypes.func,
+ ready: PropTypes.bool,
+ style: PropTypes.object, // eslint-disable-line
+};
+
+Plugin.defaultProps = {
+ className: null,
+ errorFallbackProp: null,
+ style: {},
+ ready: true,
+};
diff --git a/plugins/PluginContainer.jsx b/plugins/PluginContainer.jsx
new file mode 100644
index 000000000..98f237212
--- /dev/null
+++ b/plugins/PluginContainer.jsx
@@ -0,0 +1,42 @@
+'use client';
+
+import React from 'react';
+
+// eslint-disable-next-line import/no-extraneous-dependencies
+import PluginContainerIframe from './PluginContainerIframe';
+
+import {
+ IFRAME_PLUGIN,
+} from './data/constants';
+import { pluginConfigShape } from './data/shapes';
+
+// eslint-disable-next-line react/function-component-definition
+export default function PluginContainer({ config, ...props }) {
+ if (config === null) {
+ return null;
+ }
+
+ // this will allow for future plugin types to be inserted in the PluginErrorBoundary
+ let renderer = null;
+ switch (config.type) {
+ case IFRAME_PLUGIN:
+ renderer = (
+
+ );
+ break;
+ // istanbul ignore next: default isn't meaningful, just satisfying linter
+ default:
+ }
+
+ return (
+ renderer
+ );
+}
+
+PluginContainer.propTypes = {
+ config: pluginConfigShape,
+};
+
+PluginContainer.defaultProps = {
+ config: null,
+};
diff --git a/plugins/PluginContainerIframe.jsx b/plugins/PluginContainerIframe.jsx
new file mode 100644
index 000000000..c53ad18e0
--- /dev/null
+++ b/plugins/PluginContainerIframe.jsx
@@ -0,0 +1,99 @@
+import React, {
+ useEffect, useState,
+} from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import {
+ PLUGIN_MOUNTED,
+ PLUGIN_READY,
+ PLUGIN_RESIZE,
+} from './data/constants';
+import {
+ dispatchPluginEvent,
+ useElementSize,
+ usePluginEvent,
+} from './data/hooks';
+import { pluginConfigShape } from './data/shapes';
+
+/**
+ * Feature policy for iframe, allowing access to certain courseware-related media.
+ *
+ * We must use the wildcard (*) origin for each feature, as courseware content
+ * may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
+ * block that iframes external course content.
+
+ * This policy was selected in conference with the edX Security Working Group.
+ * Changes to it should be vetted by them (security@edx.org).
+ */
+export const IFRAME_FEATURE_POLICY = (
+ 'fullscreen; microphone *; camera *; midi *; geolocation *; encrypted-media *'
+);
+
+// eslint-disable-next-line react/function-component-definition
+export default function PluginContainerIframe({
+ config, fallback, className, ...props
+}) {
+ const { url } = config;
+ const { title, scrolling } = props;
+ const [mounted, setMounted] = useState(false);
+ const [ready, setReady] = useState(false);
+
+ const [iframeRef, iframeElement, width, height] = useElementSize();
+
+ useEffect(() => {
+ if (mounted) {
+ dispatchPluginEvent(iframeElement, {
+ type: PLUGIN_RESIZE,
+ payload: {
+ width,
+ height,
+ },
+ }, url);
+ }
+ }, [iframeElement, mounted, width, height, url]);
+
+ usePluginEvent(iframeElement, PLUGIN_MOUNTED, () => {
+ setMounted(true);
+ });
+
+ usePluginEvent(iframeElement, PLUGIN_READY, () => {
+ setReady(true);
+ });
+
+ return (
+ <>
+
+ {!ready && fallback}
+ >
+ );
+}
+
+PluginContainerIframe.propTypes = {
+ config: pluginConfigShape,
+ fallback: PropTypes.node,
+ scrolling: PropTypes.oneOf(['auto', 'yes', 'no']),
+ title: PropTypes.string,
+ className: PropTypes.string,
+};
+
+PluginContainerIframe.defaultProps = {
+ config: null,
+ fallback: null,
+ scrolling: 'auto',
+ title: null,
+ className: null,
+};
diff --git a/plugins/PluginErrorBoundary.jsx b/plugins/PluginErrorBoundary.jsx
new file mode 100644
index 000000000..3a244a009
--- /dev/null
+++ b/plugins/PluginErrorBoundary.jsx
@@ -0,0 +1,45 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { FormattedMessage } from 'react-intl';
+
+import { logError } from '@edx/frontend-platform/logging';
+
+export default class PluginErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError() {
+ // Update state so the next render will show the fallback UI.
+ return { hasError: true };
+ }
+
+ componentDidCatch(error, info) {
+ logError(error, { stack: info.componentStack });
+ }
+
+ render() {
+ if (this.state.hasError) {
+ // You can render any custom fallback UI
+ return (
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+PluginErrorBoundary.propTypes = {
+ children: PropTypes.node,
+};
+
+PluginErrorBoundary.defaultProps = {
+ children: null,
+};
diff --git a/plugins/PluginSlot.jsx b/plugins/PluginSlot.jsx
new file mode 100644
index 000000000..b860bcd5f
--- /dev/null
+++ b/plugins/PluginSlot.jsx
@@ -0,0 +1,75 @@
+/* eslint-disable no-unused-vars */
+import React, { forwardRef } from 'react';
+
+import classNames from 'classnames';
+import { Spinner } from '@edx/paragon';
+import PropTypes from 'prop-types';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+// import { usePluginSlot } from './data/hooks';
+import PluginContainer from './PluginContainer';
+
+const PluginSlot = forwardRef(({
+ as, id, intl, pluginProps, children, ...props
+}, ref) => {
+ /* the plugins below are obtained by the id passed into PluginSlot by the Host MFE. See example/src/PluginsPage.jsx
+ for an example of how PluginSlot is populated, and example/src/index.jsx for a dummy JS config that holds all plugins
+ */
+ // const { plugins, keepDefault } = usePluginSlot(id);
+
+ const { fallback } = pluginProps;
+
+ // TODO: Add internationalization to the "Loading" text on the spinner.
+ let finalFallback = (
+
+
+
+ );
+ if (fallback !== undefined) {
+ finalFallback = fallback;
+ }
+
+ let finalChildren = [];
+ // if (plugins.length > 0) {
+ // if (keepDefault) {
+ // finalChildren.push(children);
+ // }
+ // plugins.forEach((pluginConfig) => {
+ // finalChildren.push(
+ // ,
+ // );
+ // });
+ // } else {
+ finalChildren = children;
+ // }
+
+ return React.createElement(
+ as,
+ {
+ ...props,
+ ref,
+ },
+ finalChildren,
+ );
+});
+
+export default injectIntl(PluginSlot);
+
+PluginSlot.propTypes = {
+ as: PropTypes.elementType,
+ children: PropTypes.node,
+ id: PropTypes.string.isRequired,
+ intl: intlShape.isRequired,
+ pluginProps: PropTypes.object, // eslint-disable-line
+};
+
+PluginSlot.defaultProps = {
+ as: 'div',
+ children: null,
+ pluginProps: {},
+};
diff --git a/plugins/data/constants.js b/plugins/data/constants.js
new file mode 100644
index 000000000..9c834a2bf
--- /dev/null
+++ b/plugins/data/constants.js
@@ -0,0 +1,8 @@
+// TODO: We expect other plugin types to be added here, such as LTI_PLUGIN and BUILD_TIME_PLUGIN.
+export const IFRAME_PLUGIN = 'IFRAME_PLUGIN'; // loads iframe at the URL, rather than loading a JS file.
+
+// Plugin lifecycle events
+export const PLUGIN_MOUNTED = 'PLUGIN_MOUNTED';
+export const PLUGIN_READY = 'PLUGIN_READY';
+export const PLUGIN_UNMOUNTED = 'PLUGIN_UNMOUNTED';
+export const PLUGIN_RESIZE = 'PLUGIN_RESIZE';
diff --git a/plugins/data/hooks.js b/plugins/data/hooks.js
new file mode 100644
index 000000000..04d124363
--- /dev/null
+++ b/plugins/data/hooks.js
@@ -0,0 +1,96 @@
+import {
+ useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
+} from 'react';
+import { PLUGIN_MOUNTED, PLUGIN_READY, PLUGIN_UNMOUNTED } from './constants';
+
+export function useMessageEvent(srcWindow, type, callback) {
+ useLayoutEffect(() => {
+ const listener = (event) => {
+ // Filter messages to those from our source window.
+ if (event.source === srcWindow) {
+ if (event.data.type === type) {
+ callback({ type, payload: event.data.payload });
+ }
+ }
+ };
+ if (srcWindow !== null) {
+ global.addEventListener('message', listener);
+ }
+ return () => {
+ global.removeEventListener('message', listener);
+ };
+ }, [srcWindow, type, callback]);
+}
+
+export function useHostEvent(type, callback) {
+ useMessageEvent(global.parent, type, callback);
+}
+
+export function usePluginEvent(iframeElement, type, callback) {
+ const contentWindow = iframeElement ? iframeElement.contentWindow : null;
+ useMessageEvent(contentWindow, type, callback);
+}
+
+export function dispatchMessageEvent(targetWindow, message, targetOrigin) {
+ // Checking targetOrigin falsiness here since '', null or undefined would all be reasons not to
+ // try to post a message to the origin.
+ if (targetOrigin) {
+ targetWindow.postMessage(message, targetOrigin);
+ }
+}
+
+export function dispatchPluginEvent(iframeElement, message, targetOrigin) {
+ dispatchMessageEvent(iframeElement.contentWindow, message, targetOrigin);
+}
+
+export function dispatchHostEvent(message) {
+ dispatchMessageEvent(global.parent, message, global.document.referrer);
+}
+
+export function dispatchReadyEvent() {
+ dispatchHostEvent({ type: PLUGIN_READY });
+}
+
+export function dispatchMountedEvent() {
+ dispatchHostEvent({ type: PLUGIN_MOUNTED });
+}
+
+export function dispatchUnmountedEvent() {
+ dispatchHostEvent({ type: PLUGIN_UNMOUNTED });
+}
+
+export function useElementSize() {
+ const observerRef = useRef();
+
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
+
+ const [element, setElement] = useState(null);
+
+ const measuredRef = useCallback(_element => {
+ setElement(_element);
+ }, []);
+
+ useEffect(() => {
+ observerRef.current = new ResizeObserver(() => {
+ if (element) {
+ setDimensions({
+ width: element.clientWidth,
+ height: element.clientHeight,
+ });
+ setOffset({
+ x: element.offsetLeft,
+ y: element.offsetTop,
+ });
+ }
+ });
+ if (element) {
+ observerRef.current.observe(element);
+ }
+ }, [element]);
+
+ return useMemo(
+ () => ([measuredRef, element, dimensions.width, dimensions.height, offset.x, offset.y]),
+ [measuredRef, element, dimensions, offset],
+ );
+}
diff --git a/plugins/data/shapes.js b/plugins/data/shapes.js
new file mode 100644
index 000000000..75a206e33
--- /dev/null
+++ b/plugins/data/shapes.js
@@ -0,0 +1,10 @@
+/* eslint-disable import/prefer-default-export */
+import PropTypes from 'prop-types';
+import { IFRAME_PLUGIN } from './constants';
+
+export const pluginConfigShape = PropTypes.shape({
+ url: PropTypes.string.isRequired,
+ type: PropTypes.oneOf([IFRAME_PLUGIN]).isRequired,
+ // This is a place for us to put any generic props we want to pass to the component. We need it.
+ props: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+});
diff --git a/plugins/index.js b/plugins/index.js
new file mode 100644
index 000000000..1dbf8fae7
--- /dev/null
+++ b/plugins/index.js
@@ -0,0 +1,18 @@
+// export {
+// usePluginSlot,
+// } from './data/hooks';
+export {
+ default as Plugin,
+} from './Plugin';
+export {
+ default as PluginContainer,
+} from './PluginContainer';
+export {
+ default as PluginSlot,
+} from './PluginSlot';
+export {
+ IFRAME_PLUGIN,
+} from './data/constants';
+export {
+ default as PluginErrorBoundary,
+} from './PluginErrorBoundary';
diff --git a/src/index.jsx b/src/index.jsx
index 8a6bde80a..dd6d5a839 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -15,6 +15,7 @@ import {
import React from 'react';
import ReactDOM from 'react-dom';
+import { useLocation } from 'react-router-dom';
import Header from '@edx/frontend-component-header';
import Footer from '@edx/frontend-component-footer';
@@ -27,15 +28,25 @@ import Head from './head/Head';
import AppRoutes from './routes/AppRoutes';
+const RenderFooter = () => {
+ const location = useLocation();
+ return ['/u/edx/plugin'].includes(location.pathname) ? null : ;
+};
+
+const RenderHeader = () => {
+ const location = useLocation();
+ return ['/u/edx/plugin'].includes(location.pathname) ? null : ;
+};
+
subscribe(APP_READY, () => {
ReactDOM.render(
-
-
+
+
-
+
,
document.getElementById('root'),
);
diff --git a/src/profile/DateJoined.jsx b/src/profile/DateJoined.jsx
index 05ce2d66f..1fbe379f0 100644
--- a/src/profile/DateJoined.jsx
+++ b/src/profile/DateJoined.jsx
@@ -6,7 +6,6 @@ const DateJoined = ({ date }) => {
if (date == null) {
return null;
}
-
return (
this is broken as all get
+ );
+}
+
+const platformDisplayInfo = {
+ facebook: {
+ icon: faFacebook,
+ name: '',
+ },
+ twitter: {
+ icon: faTwitter,
+ name: '',
+ },
+ linkedin: {
+ icon: faLinkedin,
+ name: '',
+ },
+};
+
+class ProfilePluginPage extends React.Component {
+ componentDidMount() {
+ this.props.fetchProfile(this.props.params.username);
+ }
+
+ renderContent() {
+ const {
+ profileImage,
+ country,
+ levelOfEducation,
+ socialLinks,
+ isLoadingProfile,
+ dateJoined,
+ intl,
+ } = this.props;
+
+ if (isLoadingProfile) {
+ return ;
+ }
+
+ return (
+ }>
+
+
+ View public profile
+
+ )}
+ actions={
+ (
+
+ {socialLinks
+ .filter(({ socialLink }) => Boolean(socialLink))
+ .map(({ platform, socialLink }) => (
+
+ ))}
+
+ )
+ }
+ />
+
+
+ {this.props.params.username}
+
+
+
+
+
+
+ since
+
+
+
+
+
+ {intl.formatMessage(get(
+ eduMessages,
+ `profile.education.levels.${levelOfEducation}`,
+ eduMessages['profile.education.levels.o'],
+ ))}
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+
+ {this.renderContent()}
+
+ );
+ }
+}
+
+const SocialLink = ({ url, name, platform }) => (
+
+
+ {name}
+
+);
+
+const StaticListItem = ({ url, name, platform }) => (
+
+);
+
+ProfilePluginPage.contextType = AppContext;
+
+ProfilePluginPage.propTypes = {
+ // Account data
+ dateJoined: PropTypes.string,
+
+ // Country form data
+ country: PropTypes.string,
+
+ // Education form data
+ levelOfEducation: PropTypes.string,
+
+ // Social links form data
+ socialLinks: PropTypes.arrayOf(PropTypes.shape({
+ platform: PropTypes.string,
+ socialLink: PropTypes.string,
+ })),
+
+ // Other data we need
+ profileImage: PropTypes.shape({
+ src: PropTypes.string,
+ isDefault: PropTypes.bool,
+ }),
+ isLoadingProfile: PropTypes.bool.isRequired,
+
+ // Actions
+ fetchProfile: PropTypes.func.isRequired,
+
+ // Router
+ params: PropTypes.shape({
+ username: PropTypes.string.isRequired,
+ }).isRequired,
+
+ // i18n
+ intl: intlShape.isRequired,
+};
+
+ProfilePluginPage.defaultProps = {
+ profileImage: {},
+ levelOfEducation: null,
+ country: null,
+ socialLinks: [],
+ dateJoined: null,
+};
+
+export default connect(
+ profilePageSelector,
+ {
+ fetchProfile,
+ },
+)(injectIntl(withParams(ProfilePluginPage)));
diff --git a/src/profile/forms/PluginCountry.jsx b/src/profile/forms/PluginCountry.jsx
new file mode 100644
index 000000000..540d71c84
--- /dev/null
+++ b/src/profile/forms/PluginCountry.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { injectIntl } from '@edx/frontend-platform/i18n';
+import { Icon } from '@edx/paragon';
+import { LocationOn } from '@edx/paragon/icons';
+
+// Selectors
+import { countrySelector } from '../data/selectors';
+
+// eslint-disable-next-line react/prefer-stateless-function
+class PluginCountry extends React.Component {
+ render() {
+ const {
+ country,
+ countryMessages,
+ } = this.props;
+
+ return (
+
+
+
{countryMessages[country]}
+
+ );
+ }
+}
+
+PluginCountry.propTypes = {
+ country: PropTypes.string,
+ countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
+};
+
+PluginCountry.defaultProps = {
+ country: null,
+};
+
+export default connect(
+ countrySelector,
+ {},
+)(injectIntl(PluginCountry));
diff --git a/src/profile/index.js b/src/profile/index.js
index 4cb72e831..00a271fc3 100644
--- a/src/profile/index.js
+++ b/src/profile/index.js
@@ -3,3 +3,4 @@ export { default as saga } from './data/sagas';
export { default as ProfilePage } from './ProfilePage';
export { default as NotFoundPage } from './NotFoundPage';
export { default as messages } from './ProfilePage.messages';
+export { default as ProfilePluginPage } from './ProfilePluginPage';
diff --git a/src/profile/index.scss b/src/profile/index.scss
index cb362b1e3..d6eba2d32 100644
--- a/src/profile/index.scss
+++ b/src/profile/index.scss
@@ -162,4 +162,28 @@
position: relative;
}
}
+
+ .pgn-icons-cell-vertical {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ margin: 1px;
+ }
+ .pgn-icons-cell-horizontal {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ margin: 1px;
+ }
+
+ .profile-plugin-avatar {
+ @include media-breakpoint-up(xs) {
+ max-width: 12rem;
+ margin-right: 0;
+ margin-top: -4rem;
+ margin-bottom: 1rem;
+ }
+ }
+
}
diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx
index 1d46786b5..f0db85bb0 100644
--- a/src/routes/AppRoutes.jsx
+++ b/src/routes/AppRoutes.jsx
@@ -4,11 +4,12 @@ import {
PageWrap,
} from '@edx/frontend-platform/react';
import { Routes, Route } from 'react-router-dom';
-import { ProfilePage, NotFoundPage } from '../profile';
+import { ProfilePage, NotFoundPage, ProfilePluginPage } from '../profile';
const AppRoutes = () => (
} />
+ } />
} />
} />
diff --git a/src/routes/routes.test.jsx b/src/routes/routes.test.jsx
index 9bd40dfea..2cde161f2 100644
--- a/src/routes/routes.test.jsx
+++ b/src/routes/routes.test.jsx
@@ -15,6 +15,7 @@ jest.mock('@edx/frontend-platform/auth', () => ({
jest.mock('../profile', () => ({
ProfilePage: () => (Profile page
),
NotFoundPage: () => (Not found page
),
+ ProfilePluginPage: () => (Plugin page
),
}));
const RoutesWithProvider = (context, path) => (
@@ -54,6 +55,22 @@ describe('routes', () => {
expect(screen.getByText('Profile page')).toBeTruthy();
});
+ test('Profile Plugin page should be accessible for authenticated users', () => {
+ render(
+ RoutesWithProvider(
+ {
+ authenticatedUser: {
+ username: 'edx',
+ email: 'edx@example.com',
+ },
+ config: getConfig(),
+ },
+ '/u/edx/plugin',
+ ),
+ );
+ expect(screen.getByText('Plugin page')).toBeTruthy();
+ });
+
test('should show NotFound page for a bad route', () => {
render(
RoutesWithProvider(unauthenticatedUser, '/nonMatchingRoute'),