From 6da29373d932050189d9052749e3409565cbb9d9 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 09:56:14 -0400 Subject: [PATCH 01/12] STRIPES-861: Setup federation --- src/init.js => bootstrap.js | 3 +- src/AppRoutes.js | 4 +- src/CalloutContext.js | 4 +- src/ModulesContext.js | 7 +- src/Pluggable.js | 3 +- src/RootWithIntl.js | 7 +- src/StripesContext.js | 4 +- .../LastVisited/LastVisitedContext.js | 4 +- .../MainNav/CurrentApp/AppCtxMenuContext.js | 3 +- .../ModuleHierarchy/ModuleHierarchyContext.js | 4 +- .../ModuleTranslator/ModuleTranslator.js | 48 ------------- src/components/ModuleTranslator/index.js | 1 - src/components/RegistryLoader.js | 69 +++++++++++++++++++ src/components/index.js | 2 - src/loadRemoteComponent.js | 21 ++++++ src/locationService.js | 2 +- src/translateModules.js | 0 17 files changed, 117 insertions(+), 69 deletions(-) rename src/init.js => bootstrap.js (90%) delete mode 100644 src/components/ModuleTranslator/ModuleTranslator.js delete mode 100644 src/components/ModuleTranslator/index.js create mode 100644 src/components/RegistryLoader.js create mode 100644 src/loadRemoteComponent.js delete mode 100644 src/translateModules.js diff --git a/src/init.js b/bootstrap.js similarity index 90% rename from src/init.js rename to bootstrap.js index ebf891cce..2227a0edc 100644 --- a/src/init.js +++ b/bootstrap.js @@ -1,9 +1,10 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; + import React from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App'; +import App from './src/App'; export default function init() { const container = document.getElementById('root'); diff --git a/src/AppRoutes.js b/src/AppRoutes.js index d2cf919b4..e284af4d3 100644 --- a/src/AppRoutes.js +++ b/src/AppRoutes.js @@ -11,6 +11,7 @@ import { getEventHandlers } from './handlerService'; import { packageName } from './constants'; import { ModuleHierarchyProvider } from './components'; import events from './events'; +import loadRemoteComponent from './loadRemoteComponent'; // Process and cache "app" type modules and render the routes const AppRoutes = ({ modules, stripes }) => { @@ -22,11 +23,12 @@ const AppRoutes = ({ modules, stripes }) => { const perm = `module.${name}.enabled`; if (!stripes.hasPerm(perm)) return null; + const RemoteComponent = React.lazy(() => loadRemoteComponent(module.url, module.name)); const connect = connectFor(module.module, stripes.epics, stripes.logger); let ModuleComponent; try { - ModuleComponent = connect(module.getModule()); + ModuleComponent = connect(RemoteComponent); } catch (error) { console.error(error); // eslint-disable-line throw Error(error); diff --git a/src/CalloutContext.js b/src/CalloutContext.js index b2b0f180c..66665e70f 100644 --- a/src/CalloutContext.js +++ b/src/CalloutContext.js @@ -1,6 +1,8 @@ import React, { useContext } from 'react'; -export const CalloutContext = React.createContext(); +import { CalloutContext } from '@folio/stripes-shared-context'; + +export { CalloutContext }; export const useCallout = () => { return useContext(CalloutContext); diff --git a/src/ModulesContext.js b/src/ModulesContext.js index 2ffed1213..863526877 100644 --- a/src/ModulesContext.js +++ b/src/ModulesContext.js @@ -1,7 +1,6 @@ -import React, { useContext } from 'react'; -import { modules } from 'stripes-config'; +import { useContext } from 'react'; +import { ModulesContext } from '@folio/stripes-shared-context'; -export const ModulesContext = React.createContext(modules); +export { ModulesContext }; export default ModulesContext; export const useModules = () => useContext(ModulesContext); -export { modules as originalModules }; diff --git a/src/Pluggable.js b/src/Pluggable.js index 6d1a27baa..75c07cd31 100644 --- a/src/Pluggable.js +++ b/src/Pluggable.js @@ -1,10 +1,11 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; -import { modules } from 'stripes-config'; +import { useModules } from './ModulesContext'; import { withStripes } from './StripesContext'; import { ModuleHierarchyProvider } from './components'; const Pluggable = (props) => { + const modules = useModules(); const plugins = modules.plugin || []; const cachedPlugins = useMemo(() => { const cached = []; diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index 1dcf29c63..143387034 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -40,6 +40,7 @@ import StaleBundleWarning from './components/StaleBundleWarning'; import { StripesContext } from './StripesContext'; import { CalloutContext } from './CalloutContext'; import AuthnLogin from './components/AuthnLogin'; +import RegistryLoader from './components/RegistryLoader'; const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAuth, history = {}, queryClient }) => { const connect = connectFor('@folio/core', stripes.epics, stripes.logger); @@ -53,7 +54,8 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut return ( - + + - + + ); diff --git a/src/StripesContext.js b/src/StripesContext.js index 835262268..16c062389 100644 --- a/src/StripesContext.js +++ b/src/StripesContext.js @@ -2,7 +2,9 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import hoistNonReactStatics from 'hoist-non-react-statics'; -export const StripesContext = React.createContext(); +import { StripesContext } from '@folio/stripes-shared-context'; + +export { StripesContext }; function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; diff --git a/src/components/LastVisited/LastVisitedContext.js b/src/components/LastVisited/LastVisitedContext.js index 166e08b4a..e7c9f0f84 100644 --- a/src/components/LastVisited/LastVisitedContext.js +++ b/src/components/LastVisited/LastVisitedContext.js @@ -1,3 +1,3 @@ -import React from 'react'; +import { LastVisitedContext } from '@folio/stripes-shared-context'; -export default React.createContext({}); +export default LastVisitedContext; diff --git a/src/components/MainNav/CurrentApp/AppCtxMenuContext.js b/src/components/MainNav/CurrentApp/AppCtxMenuContext.js index 791887881..9327f1769 100644 --- a/src/components/MainNav/CurrentApp/AppCtxMenuContext.js +++ b/src/components/MainNav/CurrentApp/AppCtxMenuContext.js @@ -1,7 +1,8 @@ import React from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; +import { AppCtxMenuContext } from '@folio/stripes-shared-context'; -export const AppCtxMenuContext = React.createContext(); +export { AppCtxMenuContext }; export function withAppCtxMenu(Component) { const WrappedComponent = (props) => { diff --git a/src/components/ModuleHierarchy/ModuleHierarchyContext.js b/src/components/ModuleHierarchy/ModuleHierarchyContext.js index 265f1101b..1a5c4d177 100644 --- a/src/components/ModuleHierarchy/ModuleHierarchyContext.js +++ b/src/components/ModuleHierarchy/ModuleHierarchyContext.js @@ -1,5 +1,3 @@ -import React from 'react'; - -const ModuleHierarchyContext = React.createContext(); +import { ModuleHierarchyContext } from '@folio/stripes-shared-context'; export default ModuleHierarchyContext; diff --git a/src/components/ModuleTranslator/ModuleTranslator.js b/src/components/ModuleTranslator/ModuleTranslator.js deleted file mode 100644 index 3253cc042..000000000 --- a/src/components/ModuleTranslator/ModuleTranslator.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; - -import { ModulesContext, originalModules } from '../../ModulesContext'; - -class ModuleTranslator extends React.Component { - static propTypes = { - children: PropTypes.node, - intl: PropTypes.object, - } - - constructor(props) { - super(props); - - this.state = { - modules: this.translateModules(), - }; - } - - translateModules = () => { - return { - app: (originalModules.app || []).map(this.translateModule), - plugin: (originalModules.plugin || []).map(this.translateModule), - settings: (originalModules.settings || []).map(this.translateModule), - handler: (originalModules.handler || []).map(this.translateModule), - }; - } - - translateModule = (module) => { - const { formatMessage } = this.props.intl; - - return { - ...module, - displayName: module.displayName ? formatMessage({ id: module.displayName }) : undefined, - }; - } - - render() { - return ( - - { this.props.children } - - ); - } -} - -export default injectIntl(ModuleTranslator); diff --git a/src/components/ModuleTranslator/index.js b/src/components/ModuleTranslator/index.js deleted file mode 100644 index fe476e5a9..000000000 --- a/src/components/ModuleTranslator/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ModuleTranslator'; diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js new file mode 100644 index 000000000..0eb3a5399 --- /dev/null +++ b/src/components/RegistryLoader.js @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { ModulesContext } from '../ModulesContext'; + +// TODO: should this be handled by registry? +const parseModules = (remotes) => { + const modules = { app: [], plugin: [], settings: [], handler: [] }; + + remotes.forEach(remote => { + const { actsAs, ...rest } = remote; + actsAs.forEach(type => modules[type].push(rest)); + }); + + return modules; +}; + +// TODO: pass it via stripes config +const registryUrl = 'http://localhost:3001/registry'; + +const RegistryLoader = ({ children }) => { + const { formatMessage } = useIntl(); + const [modules, setModules] = useState(); + + useEffect(() => { + const translateModule = (module) => ({ + ...module, + displayName: module.displayName ? formatMessage({ id: module.displayName }) : undefined, + }); + + const translateModules = ({ app, plugin, settings, handler }) => ({ + app: app.map(translateModule), + plugin: plugin.map(translateModule), + settings: settings.map(translateModule), + handler: handler.map(translateModule), + }); + + const fetchRegistry = async () => { + const response = await fetch(registryUrl); + const registry = await response.json(); + const remotes = Object.entries(registry.remotes).map(([name, metadata]) => ({ name, ...metadata })); + const parsedModules = translateModules(parseModules(remotes)); + + setModules(parsedModules); + }; + + fetchRegistry(); + // We know what we are doing here so just ignore the dependency warning about 'formatMessage' + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {modules ? children : null} + + ); +}; + +RegistryLoader.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.func, + ]) +}; + + +export default RegistryLoader; diff --git a/src/components/index.js b/src/components/index.js index 015f3867a..a8ba4dee3 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -10,10 +10,8 @@ export { default as MainContainer } from './MainContainer'; export { default as MainNav } from './MainNav'; export { default as ModuleContainer } from './ModuleContainer'; export { withModule, withModules } from './Modules'; -export { default as ModuleTranslator } from './ModuleTranslator'; export { default as OrganizationLogo } from './OrganizationLogo'; export { default as OverlayContainer } from './OverlayContainer'; -export { default as Root } from './Root'; export { default as SSOLogin } from './SSOLogin'; export { default as SystemSkeleton } from './SystemSkeleton'; export { default as TitledRoute } from './TitledRoute'; diff --git a/src/loadRemoteComponent.js b/src/loadRemoteComponent.js new file mode 100644 index 000000000..687e6ee7a --- /dev/null +++ b/src/loadRemoteComponent.js @@ -0,0 +1,21 @@ +// https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers +export default async function loadRemoteComponent(remoteUrl, remoteName) { + const container = await fetch(remoteUrl) + .then((res) => res.text()) + .then((source) => { + const script = document.createElement('script'); + script.textContent = source; + document.body.appendChild(script); + return window[remoteName]; + }); + + // eslint-disable-next-line no-undef + await __webpack_init_sharing__('default'); + + // eslint-disable-next-line no-undef + await container.init(__webpack_share_scopes__.default); + + const factory = await container.get('./MainEntry'); + const Module = await factory(); + return Module; +} diff --git a/src/locationService.js b/src/locationService.js index 7ad0fb755..d3dcab3bd 100644 --- a/src/locationService.js +++ b/src/locationService.js @@ -35,7 +35,7 @@ export function isQueryResourceModule(module, location) { } export function getCurrentModule(modules, location) { - const { app, settings } = modules; + const { app, settings } = modules ?? { app: [], settings: [] }; return app.concat(settings).find(m => isQueryResourceModule(m, location)); } diff --git a/src/translateModules.js b/src/translateModules.js deleted file mode 100644 index e69de29bb..000000000 From eac40b82089d321135699c43d8181aea2a4ca88f Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 21:08:19 -0400 Subject: [PATCH 02/12] Cleanup --- src/AppRoutes.js | 1 + src/Pluggable.js | 4 +++- src/RootWithIntl.js | 1 - src/components/Root/Root.js | 8 -------- src/components/Settings/Settings.js | 6 +++--- src/gatherActions.js | 12 ------------ 6 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/AppRoutes.js b/src/AppRoutes.js index e284af4d3..4996ba727 100644 --- a/src/AppRoutes.js +++ b/src/AppRoutes.js @@ -27,6 +27,7 @@ const AppRoutes = ({ modules, stripes }) => { const connect = connectFor(module.module, stripes.epics, stripes.logger); let ModuleComponent; + try { ModuleComponent = connect(RemoteComponent); } catch (error) { diff --git a/src/Pluggable.js b/src/Pluggable.js index 75c07cd31..f411434d1 100644 --- a/src/Pluggable.js +++ b/src/Pluggable.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { useModules } from './ModulesContext'; import { withStripes } from './StripesContext'; import { ModuleHierarchyProvider } from './components'; +import loadRemoteComponent from './loadRemoteComponent'; const Pluggable = (props) => { const modules = useModules(); @@ -23,7 +24,8 @@ const Pluggable = (props) => { } if (best) { - const Child = props.stripes.connect(best.getModule()); + const RemoteComponent = React.lazy(() => loadRemoteComponent(best.url, best.name)); + const Child = props.stripes.connect(RemoteComponent); cached.push({ Child, diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index 143387034..c821a2621 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -17,7 +17,6 @@ import { MainContainer, MainNav, ModuleContainer, - ModuleTranslator, TitledRoute, Front, OIDCRedirect, diff --git a/src/components/Root/Root.js b/src/components/Root/Root.js index caf9680dd..9e4252bde 100644 --- a/src/components/Root/Root.js +++ b/src/components/Root/Root.js @@ -9,7 +9,6 @@ import { QueryClientProvider } from 'react-query'; import { ApolloProvider } from '@apollo/client'; import { ErrorBoundary } from '@folio/stripes-components'; -import { metadata, icons } from 'stripes-config'; import { ConnectContext } from '@folio/stripes-connect'; import initialReducers from '../../initialReducers'; @@ -29,11 +28,6 @@ import './Root.css'; import { withModules } from '../Modules'; import { FFetch } from './FFetch'; -if (!metadata) { - // eslint-disable-next-line no-console - console.error('No metadata harvested from package files, so you will not get app icons. Probably the stripes-core in your Stripes CLI is too old. Try `yarn global upgrade @folio/stripes-cli`'); -} - class Root extends Component { constructor(...args) { super(...args); @@ -150,8 +144,6 @@ class Root extends Component { locale, timezone, currency, - metadata, - icons, setLocale: (localeValue) => { loadTranslations(store, localeValue, defaultTranslations); }, setTimezone: (timezoneValue) => { store.dispatch(setTimezone(timezoneValue)); }, setCurrency: (currencyValue) => { store.dispatch(setCurrency(currencyValue)); }, diff --git a/src/components/Settings/Settings.js b/src/components/Settings/Settings.js index 3d687ae30..b56763f82 100644 --- a/src/components/Settings/Settings.js +++ b/src/components/Settings/Settings.js @@ -32,6 +32,7 @@ import AppIcon from '../AppIcon'; import { packageName } from '../../constants'; import RouteErrorBoundary from '../RouteErrorBoundary'; import { ModuleHierarchyProvider } from '../ModuleHierarchy'; +import loadRemoteComponent from '../../loadRemoteComponent'; import css from './Settings.css'; @@ -60,11 +61,10 @@ const Settings = ({ stripes }) => { .map((m) => { try { const connect = connectFor(m.module, stripes.epics, stripes.logger); - const module = m.getModule(); - + const RemoteComponent = React.lazy(() => loadRemoteComponent(m.url, m.name)); return { module: m, - Component: connect(module), + Component: connect(RemoteComponent), moduleStripes: stripes.clone({ connect }), }; } catch (error) { diff --git a/src/gatherActions.js b/src/gatherActions.js index abdc874e7..47c825fe9 100644 --- a/src/gatherActions.js +++ b/src/gatherActions.js @@ -1,6 +1,3 @@ -// Gather actionNames from all registered modules for hot-key mapping - -import { modules } from 'stripes-config'; import stripesComponents from '@folio/stripes-components/package'; function addKeys(moduleName, register, list) { @@ -15,15 +12,6 @@ function addKeys(moduleName, register, list) { export default function gatherActions() { const allActions = {}; - - for (const key of Object.keys(modules)) { - const set = modules[key]; - for (const key2 of Object.keys(set)) { - const module = set[key2]; - addKeys(module.module, allActions, module.actionNames); - } - } - addKeys('stripes-components', allActions, (stripesComponents.stripes || {}).actionNames); return Object.keys(allActions); From b6aa8f94a276376c38932edac520311ee4c16a42 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 21:40:31 -0400 Subject: [PATCH 03/12] Cache remote components --- src/loadRemoteComponent.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/loadRemoteComponent.js b/src/loadRemoteComponent.js index 687e6ee7a..3d13a064f 100644 --- a/src/loadRemoteComponent.js +++ b/src/loadRemoteComponent.js @@ -1,13 +1,14 @@ // https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers export default async function loadRemoteComponent(remoteUrl, remoteName) { - const container = await fetch(remoteUrl) - .then((res) => res.text()) - .then((source) => { - const script = document.createElement('script'); - script.textContent = source; - document.body.appendChild(script); - return window[remoteName]; - }); + if (!window[remoteName]) { + const response = await fetch(remoteUrl); + const source = await response.text(); + const script = document.createElement('script'); + script.textContent = source; + document.body.appendChild(script); + } + + const container = window[remoteName]; // eslint-disable-next-line no-undef await __webpack_init_sharing__('default'); @@ -17,5 +18,6 @@ export default async function loadRemoteComponent(remoteUrl, remoteName) { const factory = await container.get('./MainEntry'); const Module = await factory(); + return Module; } From ff5e9a3ebe0ec78a6bc7ad5b396d7a2e835bbed2 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 22:16:41 -0400 Subject: [PATCH 04/12] Prefetch handlers --- src/components/RegistryLoader.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js index 0eb3a5399..c17bf73a8 100644 --- a/src/components/RegistryLoader.js +++ b/src/components/RegistryLoader.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; import { ModulesContext } from '../ModulesContext'; +import loadRemoteComponent from '../loadRemoteComponent'; // TODO: should this be handled by registry? const parseModules = (remotes) => { @@ -41,6 +42,15 @@ const RegistryLoader = ({ children }) => { const registry = await response.json(); const remotes = Object.entries(registry.remotes).map(([name, metadata]) => ({ name, ...metadata })); const parsedModules = translateModules(parseModules(remotes)); + const { handler: handlerModules } = parsedModules; + + // prefetch all handlers so they can be executed in a sync way. + if (handlerModules) { + await Promise.all(handlerModules.map(async (module) => { + const component = await loadRemoteComponent(module.url, module.name); + module.getModule = () => component?.default; + })); + } setModules(parsedModules); }; From 5ab48375c41ac89ec83c8871557e1e0350a1bd41 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Thu, 27 Apr 2023 14:10:05 -0400 Subject: [PATCH 05/12] Align stripes-shared-context correctly --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 05dc18b3a..579f7cfbf 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ }, "dependencies": { "@apollo/client": "^3.2.1", + "@folio/stripes-shared-context": "1.0.0", "classnames": "^2.2.5", "core-js": "^3.26.1", "final-form": "^4.18.2", From 9fd0420860be7749883ac8f5b967cc6d7e54549b Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Thu, 27 Apr 2023 14:17:36 -0400 Subject: [PATCH 06/12] Update @folio/stripes-shared-context version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 579f7cfbf..479e15f84 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "dependencies": { "@apollo/client": "^3.2.1", - "@folio/stripes-shared-context": "1.0.0", + "@folio/stripes-shared-context": "^1.0.0", "classnames": "^2.2.5", "core-js": "^3.26.1", "final-form": "^4.18.2", From 3b4fca0e9f4b65cb7e61a5cb82483497cafdd792 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Thu, 25 May 2023 09:57:29 -0500 Subject: [PATCH 07/12] wrap Pluggable's rendered Child in suspense to isolate react-dom's hiding of ui-elements --- src/Pluggable.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Pluggable.js b/src/Pluggable.js index f411434d1..73d976a2b 100644 --- a/src/Pluggable.js +++ b/src/Pluggable.js @@ -1,5 +1,6 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, Suspense } from 'react'; import PropTypes from 'prop-types'; +import { Icon } from '@folio/stripes-components'; import { useModules } from './ModulesContext'; import { withStripes } from './StripesContext'; import { ModuleHierarchyProvider } from './components'; @@ -35,12 +36,14 @@ const Pluggable = (props) => { } return cached; - }, [plugins]); + }, [plugins, props.type]); if (cachedPlugins.length) { return cachedPlugins.map(({ plugin, Child }) => ( - + }> + + )); } From 86b32735cf9df8e1d15b284126aff70d6c57f4dc Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Thu, 8 Jun 2023 17:13:00 -0400 Subject: [PATCH 08/12] STCOR-718 load remote translations (#1309) Draft: load translations when loading remote modules Note: QueryClientProvider must be explicitly shared See https://tanstack.com/query/v3/docs/react/reference/QueryClientProvider Refs STCOR-718, STRIPES-861 --- src/RootWithIntl.js | 246 +++++++++++++++---------------- src/components/RegistryLoader.js | 92 ++++++++++-- src/components/Root/Root.js | 2 +- src/okapiReducer.js | 2 +- 4 files changed, 205 insertions(+), 137 deletions(-) diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index c821a2621..baa2a1e26 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -53,130 +53,130 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut return ( - + - - - - - { isAuthenticated || token || disableAuth ? - <> - - - - {typeof connectedStripes?.config?.staleBundleWarning === 'object' && } - - { (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && ( - - - {connectedStripes.config.useSecureTokens && } - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - )} - - - - : - - {/* The ? after :token makes that part of the path optional, so that token may optionally - be passed in via URL parameter to avoid length restrictions */} - } - /> - } - key="sso-landing" - /> - } - key="oidc-landing" - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - } - - - - + + + + + { isAuthenticated || token || disableAuth ? + <> + + + + {typeof connectedStripes?.config?.staleBundleWarning === 'object' && } + + { (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && ( + + + {connectedStripes.config.useSecureTokens && } + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + + + )} + + + + : + + {/* The ? after :token makes that part of the path optional, so that token may optionally + be passed in via URL parameter to avoid length restrictions */} + } + /> + } + key="sso-landing" + /> + } + key="oidc-landing" + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + + + + diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js index c17bf73a8..41501e2ed 100644 --- a/src/components/RegistryLoader.js +++ b/src/components/RegistryLoader.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; +import { setTranslations } from '../okapiActions'; import { ModulesContext } from '../ModulesContext'; import loadRemoteComponent from '../loadRemoteComponent'; @@ -20,28 +21,94 @@ const parseModules = (remotes) => { // TODO: pass it via stripes config const registryUrl = 'http://localhost:3001/registry'; -const RegistryLoader = ({ children }) => { +const appTranslations = []; + +/** + * loadTranslations + * return a promise that fetches translations for the given module and then + * dispatches the translations. + * @param {object} stripes + * @param {object} module info read from the registry + * + * @returns {Promise} + */ +const loadTranslations = (stripes, module) => { + const url = `${module.host}:${module.port}`; + + const parentLocale = stripes.locale.split('-')[0]; + // Since moment.js don't support translations like it or it-IT-u-nu-latn + // we need to build string like it_IT for fetch call + const loadedLocale = stripes.locale.replace('-', '_').split('-')[0]; + + // react-intl provides things like pt-BR. + // lokalise provides things like pt_BR. + // so we have to translate '-' to '_' because the translation libraries + // don't know how to talk to each other. sheesh. + const region = stripes.locale.replace('-', '_'); + + // Here we put additional condition because languages + // like Japan we need to use like ja, but with numeric system + // Japan language builds like ja_u, that incorrect. We need to be safe from that bug. + if (!appTranslations.includes(url)) { + appTranslations.push(url); + return fetch(`${url}/translations/${region}.json`) + .then((response) => { + if (response.ok) { + return response.json().then((translations) => { + // translation entries look like "key: val" + // but we want "ui-${app}.key: val" + const prefix = module.name.replace('folio_', 'ui-'); + const keyed = []; + Object.keys(translations).forEach(key => { + keyed[`${prefix}.${key}`] = translations[key]; + }); + + // I thought dispatch was synchronous, but without a return + // statement here the calling function's invocations of + // formatMessage() don't see the updated values in the store + return stripes.store.dispatch(setTranslations({ ...stripes.okapi.translations, ...keyed })); + }); + } else { + throw new Error(`Could not load translations for ${module}`); + } + }); + } else { + return Promise.resolve(); + } +}; + + +const RegistryLoader = ({ stripes, children }) => { const { formatMessage } = useIntl(); const [modules, setModules] = useState(); useEffect(() => { - const translateModule = (module) => ({ - ...module, - displayName: module.displayName ? formatMessage({ id: module.displayName }) : undefined, - }); + const translateModule = (module) => { + return loadTranslations(stripes, module) + .then(() => { + return { + ...module, + displayName: module.displayName ? formatMessage({ id: module.displayName }) : undefined, + }; + }) + .catch(e => { + // eslint-disable-next-line no-console + console.error(e); + }); + }; - const translateModules = ({ app, plugin, settings, handler }) => ({ - app: app.map(translateModule), - plugin: plugin.map(translateModule), - settings: settings.map(translateModule), - handler: handler.map(translateModule), + const translateModules = async ({ app, plugin, settings, handler }) => ({ + app: await Promise.all(app.map(translateModule)), + plugin: await Promise.all(plugin.map(translateModule)), + settings: await Promise.all(settings.map(translateModule)), + handler: await Promise.all(handler.map(translateModule)), }); const fetchRegistry = async () => { const response = await fetch(registryUrl); const registry = await response.json(); const remotes = Object.entries(registry.remotes).map(([name, metadata]) => ({ name, ...metadata })); - const parsedModules = translateModules(parseModules(remotes)); + const parsedModules = await translateModules(parseModules(remotes)); const { handler: handlerModules } = parsedModules; // prefetch all handlers so they can be executed in a sync way. @@ -72,7 +139,8 @@ RegistryLoader.propTypes = { PropTypes.arrayOf(PropTypes.node), PropTypes.node, PropTypes.func, - ]) + ]), + stripes: PropTypes.object.isRequired, }; diff --git a/src/components/Root/Root.js b/src/components/Root/Root.js index 9e4252bde..a0d93f361 100644 --- a/src/components/Root/Root.js +++ b/src/components/Root/Root.js @@ -164,7 +164,7 @@ class Root extends Component { - + Date: Fri, 9 Jun 2023 10:16:51 -0400 Subject: [PATCH 09/12] STCOR-725 load remote icons (#1317) Load remote icons, and clean up the translation loading a bit; it was still very much in draft form, and still is, but at least it doesn't throw lint errors everywhere now. Refs STCOR-725, STRIPES-861 --- src/components/RegistryLoader.js | 122 +++++++++++++++++-------------- src/components/Root/Root.js | 13 +++- src/okapiActions.js | 9 +++ src/okapiReducer.js | 3 + 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js index 41501e2ed..f150142c9 100644 --- a/src/components/RegistryLoader.js +++ b/src/components/RegistryLoader.js @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from 'react-intl'; +import { okapi } from 'stripes-config'; -import { setTranslations } from '../okapiActions'; +import { addIcon, setTranslations } from '../okapiActions'; import { ModulesContext } from '../ModulesContext'; import loadRemoteComponent from '../loadRemoteComponent'; @@ -19,9 +20,7 @@ const parseModules = (remotes) => { }; // TODO: pass it via stripes config -const registryUrl = 'http://localhost:3001/registry'; - -const appTranslations = []; +const registryUrl = okapi.registryUrl; /** * loadTranslations @@ -33,57 +32,75 @@ const appTranslations = []; * @returns {Promise} */ const loadTranslations = (stripes, module) => { - const url = `${module.host}:${module.port}`; - - const parentLocale = stripes.locale.split('-')[0]; - // Since moment.js don't support translations like it or it-IT-u-nu-latn - // we need to build string like it_IT for fetch call - const loadedLocale = stripes.locale.replace('-', '_').split('-')[0]; - - // react-intl provides things like pt-BR. - // lokalise provides things like pt_BR. - // so we have to translate '-' to '_' because the translation libraries - // don't know how to talk to each other. sheesh. - const region = stripes.locale.replace('-', '_'); - - // Here we put additional condition because languages - // like Japan we need to use like ja, but with numeric system - // Japan language builds like ja_u, that incorrect. We need to be safe from that bug. - if (!appTranslations.includes(url)) { - appTranslations.push(url); - return fetch(`${url}/translations/${region}.json`) - .then((response) => { - if (response.ok) { - return response.json().then((translations) => { - // translation entries look like "key: val" - // but we want "ui-${app}.key: val" - const prefix = module.name.replace('folio_', 'ui-'); - const keyed = []; - Object.keys(translations).forEach(key => { - keyed[`${prefix}.${key}`] = translations[key]; - }); - - // I thought dispatch was synchronous, but without a return - // statement here the calling function's invocations of - // formatMessage() don't see the updated values in the store - return stripes.store.dispatch(setTranslations({ ...stripes.okapi.translations, ...keyed })); + // construct a fully-qualified URL to load. + // + // locale strings include a name plus optional region and numbering system. + // we only care about the name and region. this stripes the numberin system + // and converts from kebab-case (the IETF standard) to snake_case (which we + // somehow adopted for our files in Lokalise). + const locale = stripes.locale.split('-u-nu-')[0].replace('-', '_'); + const url = `${module.host}:${module.port}/translations/${locale}.json`; + stripes.logger.log('core', `loading ${locale} translations for ${module.name}`); + + return fetch(url) + .then((response) => { + if (response.ok) { + return response.json().then((translations) => { + // 1. translation entries look like "key: val"; we want "ui-${app}.key: val" + // 2. module.name is snake_case (I have no idea why); we want kebab-case + const prefix = module.name.replace('folio_', 'ui-').replaceAll('_', '-'); + const keyed = []; + Object.keys(translations).forEach(key => { + keyed[`${prefix}.${key}`] = translations[key]; }); - } else { - throw new Error(`Could not load translations for ${module}`); + + const tx = { ...stripes.okapi.translations, ...keyed }; + + stripes.store.dispatch(setTranslations(tx)); + + // const tx = { ...stripes.okapi.translations, ...keyed }; + // console.log(`filters.status.active: ${tx['ui-users.filters.status.active']}`) + return stripes.setLocale(stripes.locale, tx); + }); + } else { + throw new Error(`Could not load translations for ${module}`); + } + }); +}; + +/** + * loadIcons + * Register remotely-hosted icons with stripes by dispatching addIcon + * for each element of the module's icons array. + * + * @param {object} stripes + * @param {object} module info read from the registry + * + * @returns {void} + */ +const loadIcons = (stripes, module) => { + if (module.icons && module.icons.length) { + stripes.logger.log('core', `loading icons for ${module.module}`); + module.icons.forEach(i => { + const icon = { + [i.name]: { + src: `${module.host}:${module.port}/icons/${i.name}.svg`, + alt: i.title, } - }); - } else { - return Promise.resolve(); + }; + stripes.store.dispatch(addIcon(module.module, icon)); + }); } }; - const RegistryLoader = ({ stripes, children }) => { const { formatMessage } = useIntl(); const [modules, setModules] = useState(); useEffect(() => { - const translateModule = (module) => { + const loadModuleAssets = (module) => { + loadIcons(stripes, module); + return loadTranslations(stripes, module) .then(() => { return { @@ -97,18 +114,17 @@ const RegistryLoader = ({ stripes, children }) => { }); }; - const translateModules = async ({ app, plugin, settings, handler }) => ({ - app: await Promise.all(app.map(translateModule)), - plugin: await Promise.all(plugin.map(translateModule)), - settings: await Promise.all(settings.map(translateModule)), - handler: await Promise.all(handler.map(translateModule)), + const loadModules = async ({ app, plugin, settings, handler }) => ({ + app: await Promise.all(app.map(loadModuleAssets)), + plugin: await Promise.all(plugin.map(loadModuleAssets)), + settings: await Promise.all(settings.map(loadModuleAssets)), + handler: await Promise.all(handler.map(loadModuleAssets)), }); const fetchRegistry = async () => { - const response = await fetch(registryUrl); - const registry = await response.json(); + const registry = await fetch(registryUrl).then((response) => response.json()); const remotes = Object.entries(registry.remotes).map(([name, metadata]) => ({ name, ...metadata })); - const parsedModules = await translateModules(parseModules(remotes)); + const parsedModules = await loadModules(parseModules(remotes)); const { handler: handlerModules } = parsedModules; // prefetch all handlers so they can be executed in a sync way. diff --git a/src/components/Root/Root.js b/src/components/Root/Root.js index a0d93f361..2d032d9fc 100644 --- a/src/components/Root/Root.js +++ b/src/components/Root/Root.js @@ -15,7 +15,7 @@ import initialReducers from '../../initialReducers'; import enhanceReducer from '../../enhanceReducer'; import createApolloClient from '../../createApolloClient'; import createReactQueryClient from '../../createReactQueryClient'; -import { setSinglePlugin, setBindings, setIsAuthenticated, setOkapiToken, setTimezone, setCurrency, updateCurrentUser } from '../../okapiActions'; +import { addIcon, setSinglePlugin, setBindings, setIsAuthenticated, setOkapiToken, setTimezone, setCurrency, updateCurrentUser } from '../../okapiActions'; import { loadTranslations, checkOkapiSession } from '../../loginServices'; import { getQueryResourceKey, getCurrentModule } from '../../locationService'; import Stripes from '../../Stripes'; @@ -111,7 +111,7 @@ class Root extends Component { } render() { - const { logger, store, epics, config, okapi, actionNames, token, isAuthenticated, disableAuth, currentUser, currentPerms, locale, defaultTranslations, timezone, currency, plugins, bindings, discovery, translations, history, serverDown } = this.props; + const { logger, store, epics, config, okapi, actionNames, token, isAuthenticated, disableAuth, currentUser, currentPerms, icons, locale, defaultTranslations, timezone, currency, plugins, bindings, discovery, translations, history, serverDown } = this.props; if (serverDown) { // note: this isn't i18n'ed because we haven't rendered an IntlProvider yet. return
Error: server is forbidden, unreachable or down. Clear the cookies? Use incognito mode? VPN issue?
; @@ -144,7 +144,9 @@ class Root extends Component { locale, timezone, currency, - setLocale: (localeValue) => { loadTranslations(store, localeValue, defaultTranslations); }, + icons, + addIcon: (key, icon) => { store.dispatch(addIcon(key, icon)); }, + setLocale: (localeValue, tx) => { return loadTranslations(store, localeValue, { ...defaultTranslations, ...tx }); }, setTimezone: (timezoneValue) => { store.dispatch(setTimezone(timezoneValue)); }, setCurrency: (currencyValue) => { store.dispatch(setCurrency(currencyValue)); }, updateUser: (userValue) => { store.dispatch(updateCurrentUser(userValue)); }, @@ -164,7 +166,7 @@ class Root extends Component { - + Date: Fri, 4 Aug 2023 16:08:46 -0400 Subject: [PATCH 10/12] STCOR-718 correctly set apps' localized displayName Correctly set each apps' localized `displayName` attribute. It isn't totally clear to me why this doesn't work via `formattedMessage`. It seems that something is happening asynchronously that we don't realize is async, and therefore don't await, and then we end up calling `formatMessage()` before the translations have been pushed to the store. In any case, pulling the value straight from the translations array works fine. Refs STCOR-718 --- src/components/RegistryLoader.js | 125 +++++++++++++++++++++---------- src/loginServices.js | 8 +- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js index f150142c9..665b05d6e 100644 --- a/src/components/RegistryLoader.js +++ b/src/components/RegistryLoader.js @@ -1,13 +1,19 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; import { okapi } from 'stripes-config'; -import { addIcon, setTranslations } from '../okapiActions'; import { ModulesContext } from '../ModulesContext'; import loadRemoteComponent from '../loadRemoteComponent'; -// TODO: should this be handled by registry? +/** + * parseModules + * Map the list of applications to a hash keyed by acts-as type (app, plugin, + * settings, handler) where the value of each is an array of corresponding + * applications. + * + * @param {array} remotes + * @returns {app: [], plugin: [], settings: [], handler: []} + */ const parseModules = (remotes) => { const modules = { app: [], plugin: [], settings: [], handler: [] }; @@ -19,13 +25,11 @@ const parseModules = (remotes) => { return modules; }; -// TODO: pass it via stripes config -const registryUrl = okapi.registryUrl; - /** * loadTranslations - * return a promise that fetches translations for the given module and then - * dispatches the translations. + * return a promise that fetches translations for the given module, + * dispatches setLocale, and then returns the translations. + * * @param {object} stripes * @param {object} module info read from the registry * @@ -56,11 +60,12 @@ const loadTranslations = (stripes, module) => { const tx = { ...stripes.okapi.translations, ...keyed }; - stripes.store.dispatch(setTranslations(tx)); + // stripes.store.dispatch(setTranslations(tx)); // const tx = { ...stripes.okapi.translations, ...keyed }; // console.log(`filters.status.active: ${tx['ui-users.filters.status.active']}`) - return stripes.setLocale(stripes.locale, tx); + stripes.setLocale(stripes.locale, tx); + return tx; }); } else { throw new Error(`Could not load translations for ${module}`); @@ -82,52 +87,92 @@ const loadIcons = (stripes, module) => { if (module.icons && module.icons.length) { stripes.logger.log('core', `loading icons for ${module.module}`); module.icons.forEach(i => { + stripes.logger.log('core', ` > ${i.name}`); + const icon = { [i.name]: { src: `${module.host}:${module.port}/icons/${i.name}.svg`, alt: i.title, } }; - stripes.store.dispatch(addIcon(module.module, icon)); + stripes.addIcon(module.module, icon); }); } }; +/** + * loadModuleAssets + * Load a module's icons, translations, and sounds. + * @param {object} stripes + * @param {object} module info read from the registry + * @returns {} copy of the module, plus the key `displayName` containing its localized name + */ +const loadModuleAssets = (stripes, module) => { + // register icons + loadIcons(stripes, module); + + // register sounds + // TODO loadSounds(stripes, module); + + // register translations + return loadTranslations(stripes, module) + .then((tx) => { + return { + ...module, + // tx[module.displayName] instead of formatMessage({ id: module.displayName}) + // because ... I'm not sure exactly. I suspect the answer is that we're doing + // something async somewhere but not realizing it, and therefore not returning + // a promise. thus, loadTranslations returns before it's actually done loading + // translations, and calling formatMessage(...) here executes before the new + // values are loaded. + // + // TODO: update when modules are served with compiled translations + displayName: module.displayName ? tx[module.displayName] : module.module, + }; + }) + .catch(e => { + // eslint-disable-next-line no-console + console.error(e); + }); +}; + +/** + * loadModules + * NB: this means multi-type modules, i.e. those like `actsAs: [app, settings]` + * will be loaded multiple times. I'm not sure that's right. + * @param {props} + * @returns Promise + */ +const loadModules = async ({ app, plugin, settings, handler, stripes }) => ({ + app: await Promise.all(app.map(i => loadModuleAssets(stripes, i))), + plugin: await Promise.all(plugin.map(i => loadModuleAssets(stripes, i))), + settings: await Promise.all(settings.map(i => loadModuleAssets(stripes, i))), + handler: await Promise.all(handler.map(i => loadModuleAssets(stripes, i))), +}); + + +/** + * Registry Loader + * @param {object} stripes + * @param {*} children + * @returns + */ const RegistryLoader = ({ stripes, children }) => { - const { formatMessage } = useIntl(); const [modules, setModules] = useState(); + // read the list of registered apps from the registry, useEffect(() => { - const loadModuleAssets = (module) => { - loadIcons(stripes, module); - - return loadTranslations(stripes, module) - .then(() => { - return { - ...module, - displayName: module.displayName ? formatMessage({ id: module.displayName }) : undefined, - }; - }) - .catch(e => { - // eslint-disable-next-line no-console - console.error(e); - }); - }; - - const loadModules = async ({ app, plugin, settings, handler }) => ({ - app: await Promise.all(app.map(loadModuleAssets)), - plugin: await Promise.all(plugin.map(loadModuleAssets)), - settings: await Promise.all(settings.map(loadModuleAssets)), - handler: await Promise.all(handler.map(loadModuleAssets)), - }); - const fetchRegistry = async () => { - const registry = await fetch(registryUrl).then((response) => response.json()); + // read the list of registered apps + const registry = await fetch(okapi.registryUrl).then((response) => response.json()); + + // remap registry from an object shaped like { key1: app1, key2: app2, ...} + // to an array shaped like [ { name: key1, ...app1 }, { name: key2, ...app2 } ...] const remotes = Object.entries(registry.remotes).map(([name, metadata]) => ({ name, ...metadata })); - const parsedModules = await loadModules(parseModules(remotes)); - const { handler: handlerModules } = parsedModules; + const parsedModules = await loadModules({ stripes, ...parseModules(remotes) }); // prefetch all handlers so they can be executed in a sync way. + const { handler: handlerModules } = parsedModules; if (handlerModules) { await Promise.all(handlerModules.map(async (module) => { const component = await loadRemoteComponent(module.url, module.name); @@ -139,8 +184,8 @@ const RegistryLoader = ({ stripes, children }) => { }; fetchRegistry(); - // We know what we are doing here so just ignore the dependency warning about 'formatMessage' - // eslint-disable-next-line react-hooks/exhaustive-deps + // We know what we are doing here so just ignore the dependency warning about 'formatMessage' + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/src/loginServices.js b/src/loginServices.js index 5a38dc465..96dc17f4f 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -220,14 +220,16 @@ export function loadTranslations(store, locale, defaultTranslations = {}) { // Here we put additional condition because languages // like Japan we need to use like ja, but with numeric system // Japan language builds like ja_u, that incorrect. We need to be safe from that bug. - return fetch(translations[region] ? translations[region] : - translations[loadedLocale] || translations[[parentLocale]]) + const translationsUrl = translations[region] ?? (translations[loadedLocale] || translations[parentLocale]); + return fetch(translationsUrl) .then((response) => { if (response.ok) { - response.json().then((stripesTranslations) => { + return response.json().then((stripesTranslations) => { store.dispatch(setTranslations(Object.assign(stripesTranslations, defaultTranslations))); store.dispatch(setLocale(locale)); }); + } else { + return Promise.reject(new Error(`Could not load translations from ${translationsUrl}`)); } }); } From f66ad620f7d5e62a5b6cf09d3efea32a7c3a2244 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Fri, 4 Aug 2023 16:11:19 -0400 Subject: [PATCH 11/12] STCOR-725 correctly load multiple icons per app Correctly handle multiple icons per application. Refs STCOR-725 --- src/okapiReducer.js | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/okapiReducer.js b/src/okapiReducer.js index 05f17ea4e..ecf6a30fb 100644 --- a/src/okapiReducer.js +++ b/src/okapiReducer.js @@ -134,8 +134,34 @@ export default function okapiReducer(state = {}, action) { case OKAPI_REDUCER_ACTIONS.TOGGLE_RTR_MODAL: { return { ...state, rtrModalIsVisible: action.isVisible }; } - case OKAPI_REDUCER_ACTIONS.ADD_ICON: - return { ...state, icons: { ...state.icons, [action.key]: action.icon } }; + + /** + * state.icons looks like + * { + * "@folio/some-app": { + * app: { alt, src}, + * otherIcon: { alt, src } + * }, + * "@folio/other-app": { app: ...} + * } + * + * action.key looks like @folio/some-app or @folio/other-app + * action.icon looks like { alt: ... } or { otherIcon: ... } + */ + case OKAPI_REDUCER_ACTIONS.ADD_ICON: { + let val = action.icon; + + // if there are already icons defined for this key, + // add this payload to them + if (state.icons?.[action.key]) { + val = { + ...state.icons[action.key], + ...action.icon, + }; + } + + return { ...state, icons: { ...state.icons, [action.key]: val } }; + } default: return state; From 44bd6ddc9cd7004fa6e91a06924029ff1087bb78 Mon Sep 17 00:00:00 2001 From: Zak Burke Date: Wed, 4 Dec 2024 14:56:30 -0500 Subject: [PATCH 12/12] refactor catch up Major refactoring in stripes-core between this branch's initial work and the present lead to some discrepancies. The only change of note here, I think, is the relocation of `` from ModuleRoutes down into AppRoutes. It isn't clear to me why that was necessary or why it worked. It was just a hunch that I tried ... and it worked. Prior to that change, AppRoutes would get stuck in a render loop, infinitely reloading (yes, even the memoized functions). I don't have a good explanation for the bug or the fix. --- bootstrap.js | 9 +- index.js | 1 - src/AppRoutes.js | 63 +++---- src/ModuleRoutes.js | 6 +- src/Pluggable.js | 2 + src/RootWithIntl.js | 248 +++++++++++++------------- src/components/About/WarningBanner.js | 4 +- src/loginServices.js | 2 +- 8 files changed, 165 insertions(+), 170 deletions(-) diff --git a/bootstrap.js b/bootstrap.js index 2227a0edc..bdb96b828 100644 --- a/bootstrap.js +++ b/bootstrap.js @@ -1,13 +1,10 @@ import 'core-js/stable'; import 'regenerator-runtime/runtime'; - import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './src/App'; -export default function init() { - const container = document.getElementById('root'); - const root = createRoot(container); - root.render(); -} +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/index.js b/index.js index 3a9510d40..eb5c7e073 100644 --- a/index.js +++ b/index.js @@ -49,7 +49,6 @@ export { supportedNumberingSystems } from './src/loginServices'; export { userLocaleConfig } from './src/loginServices'; export * from './src/consortiaServices'; export { default as queryLimit } from './src/queryLimit'; -export { default as init } from './src/init'; /* localforage wrappers hide the session key */ export { getOkapiSession, getTokenExpiry, setTokenExpiry } from './src/loginServices'; diff --git a/src/AppRoutes.js b/src/AppRoutes.js index 4996ba727..f01cc26a4 100644 --- a/src/AppRoutes.js +++ b/src/AppRoutes.js @@ -1,8 +1,9 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, Suspense } from 'react'; import { Route } from 'react-router-dom'; import PropTypes from 'prop-types'; import { connectFor } from '@folio/stripes-connect'; +import { LoadingView } from '@folio/stripes-components'; import { StripesContext } from './StripesContext'; import TitleManager from './components/TitleManager'; @@ -50,37 +51,39 @@ const AppRoutes = ({ modules, stripes }) => { }, [modules.app, stripes]); return cachedModules.map(({ ModuleComponent, connect, module, name, moduleStripes, stripes: propsStripes, displayName }) => ( - { - const data = { displayName, name }; + }> + { + const data = { displayName, name }; - // allow SELECT_MODULE handlers to intervene - const handlerComponents = getEventHandlers(events.SELECT_MODULE, moduleStripes, modules.handler, data); - if (handlerComponents.length) { - return handlerComponents.map(Handler => ()); - } + // allow SELECT_MODULE handlers to intervene + const handlerComponents = getEventHandlers(events.SELECT_MODULE, moduleStripes, modules.handler, data); + if (handlerComponents.length) { + return handlerComponents.map(Handler => ()); + } - return ( - - -
- - - - - -
-
-
- ); - }} - /> + return ( + + +
+ + + + + +
+
+
+ ); + }} + /> +
)); }; diff --git a/src/ModuleRoutes.js b/src/ModuleRoutes.js index d1dc65ce0..08046067f 100644 --- a/src/ModuleRoutes.js +++ b/src/ModuleRoutes.js @@ -71,11 +71,7 @@ function ModuleRoutes({ stripes }) { ); } - return ( - }> - - - ); + return ; }} ); diff --git a/src/Pluggable.js b/src/Pluggable.js index 73d976a2b..6685366e1 100644 --- a/src/Pluggable.js +++ b/src/Pluggable.js @@ -1,6 +1,8 @@ import React, { useMemo, Suspense } from 'react'; import PropTypes from 'prop-types'; + import { Icon } from '@folio/stripes-components'; + import { useModules } from './ModulesContext'; import { withStripes } from './StripesContext'; import { ModuleHierarchyProvider } from './components'; diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index baa2a1e26..118da5bd4 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -53,131 +53,129 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut return ( - - - - - - - { isAuthenticated || token || disableAuth ? - <> - - - - {typeof connectedStripes?.config?.staleBundleWarning === 'object' && } - - { (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && ( - - - {connectedStripes.config.useSecureTokens && } - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - - - )} - - - - : - - {/* The ? after :token makes that part of the path optional, so that token may optionally - be passed in via URL parameter to avoid length restrictions */} - } - /> - } - key="sso-landing" - /> - } - key="oidc-landing" - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - - } - - - - - + + + + + + { isAuthenticated || token || disableAuth ? + <> + + + + {typeof connectedStripes?.config?.staleBundleWarning === 'object' && } + + { (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && ( + + + {connectedStripes.config.useSecureTokens && } + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + + + )} + + + + : + + {/* The ? after :token makes that part of the path optional, so that token may optionally + be passed in via URL parameter to avoid length restrictions */} + } + /> + } + key="sso-landing" + /> + } + key="oidc-landing" + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + + + + diff --git a/src/components/About/WarningBanner.js b/src/components/About/WarningBanner.js index 1ff3867b2..528f806cc 100644 --- a/src/components/About/WarningBanner.js +++ b/src/components/About/WarningBanner.js @@ -48,7 +48,7 @@ const WarningBanner = ({ {missingModulesMsg} @@ -61,7 +61,7 @@ const WarningBanner = ({ {incompatibleModuleMsg} diff --git a/src/loginServices.js b/src/loginServices.js index 96dc17f4f..17023704a 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -252,7 +252,7 @@ function dispatchLocale(url, store, tenant) { }) .then((response) => { if (response.ok) { - response.json().then((json) => { + return response.json().then((json) => { if (json.configs?.length) { const localeValues = JSON.parse(json.configs[0].value); const { locale, timezone, currency } = localeValues;