From 251f96dd43119437b774b9e11db4c3a0bb9ef0e8 Mon Sep 17 00:00:00 2001 From: Viktor Rusakov Date: Thu, 2 Feb 2023 09:41:12 +0200 Subject: [PATCH] feat: add ability to dynamically load theme overrides --- .env.development | 3 ++- .env.test | 1 + package-lock.json | 46 +++++++++++++++++++++++++++++++++++---- package.json | 1 + src/config.js | 2 ++ src/react/AppProvider.jsx | 26 +++++++++++++++++++--- 6 files changed, 71 insertions(+), 8 deletions(-) diff --git a/.env.development b/.env.development index 21974f265..9b8471f9d 100644 --- a/.env.development +++ b/.env.development @@ -27,4 +27,5 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico IGNORED_ERROR_REGEX= MFE_CONFIG_API_URL= APP_ID= -SUPPORT_URL=https://support.edx.org \ No newline at end of file +SUPPORT_URL=https://support.edx.org +THEME_OVERRIDE_URL= diff --git a/.env.test b/.env.test index f25231a3b..9b8471f9d 100644 --- a/.env.test +++ b/.env.test @@ -28,3 +28,4 @@ IGNORED_ERROR_REGEX= MFE_CONFIG_API_URL= APP_ID= SUPPORT_URL=https://support.edx.org +THEME_OVERRIDE_URL= diff --git a/package-lock.json b/package-lock.json index c035b507e..5720d422a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "lodash.merge": "4.6.2", "lodash.snakecase": "4.1.1", "pubsub-js": "1.9.4", + "react-helmet": "^6.1.0", "react-intl": "^5.25.0", "universal-cookie": "4.0.4" }, @@ -16372,8 +16373,7 @@ "node_modules/react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", - "dev": true + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "node_modules/react-focus-lock": { "version": "2.9.1", @@ -16425,6 +16425,20 @@ } } }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-intl": { "version": "5.25.1", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz", @@ -16664,6 +16678,14 @@ "react": ">=15" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.0.tgz", @@ -33087,8 +33109,7 @@ "react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", - "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==", - "dev": true + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, "react-focus-lock": { "version": "2.9.1", @@ -33119,6 +33140,17 @@ "use-sidecar": "^1.1.2" } }, + "react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + } + }, "react-intl": { "version": "5.25.1", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz", @@ -33292,6 +33324,12 @@ "tiny-warning": "^1.0.0" } }, + "react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "requires": {} + }, "react-style-singleton": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.0.tgz", diff --git a/package.json b/package.json index 62f10b1cd..72fba576a 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "lodash.merge": "4.6.2", "lodash.snakecase": "4.1.1", "pubsub-js": "1.9.4", + "react-helmet": "^6.1.0", "react-intl": "^5.25.0", "universal-cookie": "4.0.4" }, diff --git a/src/config.js b/src/config.js index 139ac5415..b316e19d6 100644 --- a/src/config.js +++ b/src/config.js @@ -72,6 +72,7 @@ let config = { MFE_CONFIG_API_URL: process.env.MFE_CONFIG_API_URL, APP_ID: process.env.APP_ID, SUPPORT_URL: process.env.SUPPORT_URL, + THEME_OVERRIDE_URL: process.env.THEME_OVERRIDE_URL, }; /** @@ -203,4 +204,5 @@ export function ensureConfig(keys, requester = 'unspecified application code') { * @property {string} MFE_CONFIG_API_URL * @property {string} APP_ID * @property {string} SUPPORT_URL + * @property {string} THEME_OVERRIDE_URL */ diff --git a/src/react/AppProvider.jsx b/src/react/AppProvider.jsx index 36bc12037..823d96680 100644 --- a/src/react/AppProvider.jsx +++ b/src/react/AppProvider.jsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Router } from 'react-router-dom'; @@ -48,6 +48,7 @@ export default function AppProvider({ store, children }) { const [config, setConfig] = useState(getConfig()); const [authenticatedUser, setAuthenticatedUser] = useState(getAuthenticatedUser()); const [locale, setLocale] = useState(getLocale()); + const [themeLoaded, setThemeLoaded] = useState(false); useAppEvent(AUTHENTICATED_USER_CHANGED, () => { setAuthenticatedUser(getAuthenticatedUser()); @@ -61,8 +62,28 @@ export default function AppProvider({ store, children }) { setLocale(getLocale()); }); + useEffect(() => { + if (config.THEME_OVERRIDE_URL) { + const themeLink = document.createElement('link'); + themeLink.href = config.THEME_OVERRIDE_URL; + themeLink.rel = 'stylesheet'; + themeLink.type = 'text/css'; + themeLink.onload = () => setThemeLoaded(true); + themeLink.onerror = () => setThemeLoaded(true); + + document.head.appendChild(themeLink); + + return () => document.head.removeChild(themeLink); + } + setThemeLoaded(true); + }, [config.THEME_OVERRIDE_URL]); + const appContextValue = useMemo(() => ({ authenticatedUser, config, locale }), [authenticatedUser, config, locale]); + if (!themeLoaded) { + return null; + } + return ( @@ -81,8 +102,7 @@ export default function AppProvider({ store, children }) { } AppProvider.propTypes = { - // eslint-disable-next-line react/forbid-prop-types - store: PropTypes.object, + store: PropTypes.shape({}), children: PropTypes.node.isRequired, };