From 6da29373d932050189d9052749e3409565cbb9d9 Mon Sep 17 00:00:00 2001 From: Michal Kuklis Date: Tue, 25 Apr 2023 09:56:14 -0400 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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; From 72a3f544b2118a2a020382b030237942c4740286 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 14 Nov 2025 15:07:00 -0600 Subject: [PATCH 13/18] add registry to token util's set of paths for creds --- src/components/Root/token-util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Root/token-util.js b/src/components/Root/token-util.js index 2ea9097e5..4aa5ed519 100644 --- a/src/components/Root/token-util.js +++ b/src/components/Root/token-util.js @@ -90,6 +90,7 @@ export const isAuthenticationRequest = (resource, oUrl) => { '/bl-users/login-with-expiry', '/bl-users/_self', '/users-keycloak/_self', + '/registry' ]; return !!permissible.find(i => string.startsWith(`${oUrl}${i}`)); From 3cf47fd49765825674bdaeabc10cc89d3b42f7ff Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 14 Nov 2025 15:09:16 -0600 Subject: [PATCH 14/18] ship context out to stripes-shared-context --- src/App.js | 2 +- src/ModulesContext.js | 7 +------ src/components/MainNav/AppOrderProvider.js | 16 +++++++++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/App.js b/src/App.js index db89ff325..6d7e7429c 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,7 @@ import React, { Component, StrictMode } from 'react'; import PropTypes from 'prop-types'; import { okapi as okapiConfig, config } from 'stripes-config'; import merge from 'lodash/merge'; +import { modulesInitialState } from '@folio/stripes-shared-context'; import AppConfigError from './components/AppConfigError'; import connectErrorEpic from './connectErrorEpic'; @@ -11,7 +12,6 @@ import configureStore from './configureStore'; import gatherActions from './gatherActions'; import { destroyStore } from './mainActions'; import { getModules } from './entitlementService'; -import { modulesInitialState } from './ModulesContext'; import css from './components/SessionEventContainer/style.css'; import Root from './components/Root'; diff --git a/src/ModulesContext.js b/src/ModulesContext.js index 863526877..0c5b57d15 100644 --- a/src/ModulesContext.js +++ b/src/ModulesContext.js @@ -1,6 +1 @@ -import { useContext } from 'react'; -import { ModulesContext } from '@folio/stripes-shared-context'; - -export { ModulesContext }; -export default ModulesContext; -export const useModules = () => useContext(ModulesContext); +export { ModulesContext, modulesInitialState, useModules } from '@folio/stripes-shared-context'; diff --git a/src/components/MainNav/AppOrderProvider.js b/src/components/MainNav/AppOrderProvider.js index 4b790da10..8eeaa6bfe 100644 --- a/src/components/MainNav/AppOrderProvider.js +++ b/src/components/MainNav/AppOrderProvider.js @@ -2,10 +2,9 @@ import { createContext, useContext, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { useIntl } from 'react-intl'; import { useQuery } from 'react-query'; +import isArray from 'lodash/isArray'; -import { useStripes } from '../../StripesContext'; -import { useModules } from '../../ModulesContext'; -import { LastVisitedContext } from '../LastVisited'; +import { LastVisitedContext, useModules, useStripes } from '@folio/stripes-shared-context'; import usePreferences from '../../hooks/usePreferences'; import { packageName } from '../../constants'; import settingsIcon from './settings.svg'; @@ -39,11 +38,11 @@ export const AppOrderContext = createContext({ * Function to update the preference. Accepts an list of objects with shape: * { name: string - the module package name, sans scope and `ui-` prefix } */ - updateList: () => {}, + updateList: () => { }, /** * Function to delete any the app order preference and reset the list. */ - reset: () => {}, + reset: () => { }, }); // hook for AppOrderContext consumption. @@ -103,6 +102,13 @@ function getAllowedApps(appModules, stripes, pathname, lastVisited, formatMessag route: SETTINGS_ROUTE }); } + + // use translated displayName rather that ast object; + apps.forEach((app) => { + if (isArray(app.displayName)) { + app.displayName = app.displayName[0].value; + } + }); return apps.toSorted((a, b) => a.displayName.localeCompare(b.displayName)); } From 21dd6adbc6aad646851d06c76c6bc6c3e1a8eff1 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 14 Nov 2025 15:09:46 -0600 Subject: [PATCH 15/18] preload all the things --- src/components/RegistryLoader.js | 120 ++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js index 665b05d6e..c99905e46 100644 --- a/src/components/RegistryLoader.js +++ b/src/components/RegistryLoader.js @@ -14,13 +14,50 @@ import loadRemoteComponent from '../loadRemoteComponent'; * @param {array} remotes * @returns {app: [], plugin: [], settings: [], handler: []} */ -const parseModules = (remotes) => { +const parseModules = async (remotes) => { const modules = { app: [], plugin: [], settings: [], handler: [] }; - remotes.forEach(remote => { - const { actsAs, ...rest } = remote; - actsAs.forEach(type => modules[type].push(rest)); - }); + // TODO finish prefetching modules here.... + try { + const loaderArray = []; + remotes.forEach(async remote => { + const { name, url } = remote; + // setting getModule for backwards compatibility with parts of stripes that call it.. + loaderArray.push(loadRemoteComponent(url, name)); + }); + await Promise.all(loaderArray); + remotes.forEach((remote, i) => { + const { actsAs, name, url, ...rest } = remote; + const getModule = () => loaderArray[i].default; + actsAs.forEach(type => modules[type].push({ actsAs, name, url, getModule, ...rest })); + }); + } catch (e) { + console.error('Error parsing modules from registry', e); + } + + return modules; +}; + +const preloadModules = async (remotes) => { + const modules = { app: [], plugin: [], settings: [], handler: [] }; + + // TODO finish prefetching modules here.... + try { + const loaderArray = []; + remotes.forEach(async remote => { + const { name, url } = remote; + // setting getModule for backwards compatibility with parts of stripes that call it.. + loaderArray.push(loadRemoteComponent(url, name)); + }); + await Promise.all(loaderArray); + remotes.forEach((remote, i) => { + const { actsAs, name, url, ...rest } = remote; + const getModule = () => loaderArray[i].default; + actsAs.forEach(type => modules[type].push({ actsAs, name, url, getModule, ...rest })); + }); + } catch (e) { + console.error('Error parsing modules from registry', e); + } return modules; }; @@ -52,13 +89,13 @@ const loadTranslations = (stripes, module) => { 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]; - }); + // const prefix = module.name.replace('folio_', 'ui-').replaceAll('_', '-'); + // const keyed = []; + // Object.keys(translations).forEach(key => { + // keyed[`${prefix}.${key}`] = translations[key]; + // }); - const tx = { ...stripes.okapi.translations, ...keyed }; + const tx = { ...stripes.okapi.translations, ...translations }; // stripes.store.dispatch(setTranslations(tx)); @@ -117,17 +154,29 @@ const loadModuleAssets = (stripes, module) => { // register translations return loadTranslations(stripes, module) .then((tx) => { + // 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. + // + // when translations are compiled, the value of the tx[module.displayName] is an array + // containing a single object with shape { type: 'messageFormatPattern', value: 'the actual string' } + // so we have to extract the value from that structure. + let newDisplayName; + if (module.displayName) { + if (typeof tx[module.displayName] === 'string') { + newDisplayName = tx[module.displayName]; + } else { + newDisplayName = tx[module.displayName][0].value; + } + } + 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, + displayName: module.displayName ? + newDisplayName : module.module, }; }) .catch(e => { @@ -150,6 +199,9 @@ const loadModules = async ({ app, plugin, settings, handler, stripes }) => ({ handler: await Promise.all(handler.map(i => loadModuleAssets(stripes, i))), }); +const loadAllModuleAssets = async (stripes, remotes) => { + return Promise.all(remotes.map((r) => loadModuleAssets(stripes, r))); +}; /** * Registry Loader @@ -169,17 +221,31 @@ const RegistryLoader = ({ stripes, children }) => { // 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({ stripes, ...parseModules(remotes) }); + // load module assets, then load modules... + const remotesWithLoadedAssets = await loadAllModuleAssets(stripes, remotes); + // const parsedModules = await loadModules({ stripes, ...parseModules(remotes) }); + const parsedModules = await preloadModules(remotesWithLoadedAssets); // 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); - module.getModule = () => component?.default; - })); + // const { handler: handlerModules } = parsedModules; + // if (handlerModules) { + // await Promise.all(handlerModules.map(async (module) => { + // const component = await loadRemoteComponent(module.url, module.name); + // module.getModule = () => component?.default; + // })); + // } + + // preload all modules... + for (const type in parsedModules) { + if (parsedModules[type]) { + parsedModules[type].forEach(async (module) => { + const loadedModule = await loadRemoteComponent(module.url, module.name); + module.getModule = () => loadedModule?.default; + }); + } } + // prefetch setModules(parsedModules); }; From 3deb33c8916ed52b3e3d7a8af15e4c65590f44cf Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 14 Nov 2025 15:10:53 -0600 Subject: [PATCH 16/18] go back to memo'd version --- src/Pluggable.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Pluggable.js b/src/Pluggable.js index e0f61e8d2..91bce4e53 100644 --- a/src/Pluggable.js +++ b/src/Pluggable.js @@ -6,7 +6,6 @@ import { Loading } from '@folio/stripes-components'; import { useModules } from './ModulesContext'; import { withStripes } from './StripesContext'; import { ModuleHierarchyProvider } from './components'; -import loadRemoteComponent from './loadRemoteComponent'; const Pluggable = (props) => { const modules = useModules(); @@ -27,8 +26,7 @@ const Pluggable = (props) => { } if (best) { - const RemoteComponent = React.lazy(() => loadRemoteComponent(best.url, best.name)); - const Child = props.stripes.connect(RemoteComponent); + const Child = props.stripes.connect(best.getModule()); cached.push({ Child, From c7563335bdb6783bc8de662efaee721155e79d16 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Fri, 14 Nov 2025 15:53:18 -0600 Subject: [PATCH 17/18] clean up --- src/components/RegistryLoader.js | 88 +++++++------------------------- 1 file changed, 18 insertions(+), 70 deletions(-) diff --git a/src/components/RegistryLoader.js b/src/components/RegistryLoader.js index c99905e46..c0426e5bd 100644 --- a/src/components/RegistryLoader.js +++ b/src/components/RegistryLoader.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { okapi } from 'stripes-config'; @@ -6,7 +6,8 @@ import { ModulesContext } from '../ModulesContext'; import loadRemoteComponent from '../loadRemoteComponent'; /** - * parseModules + * preloadModules + * Loads each module code and sets up its getModule function. * 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. @@ -14,46 +15,23 @@ import loadRemoteComponent from '../loadRemoteComponent'; * @param {array} remotes * @returns {app: [], plugin: [], settings: [], handler: []} */ -const parseModules = async (remotes) => { - const modules = { app: [], plugin: [], settings: [], handler: [] }; - - // TODO finish prefetching modules here.... - try { - const loaderArray = []; - remotes.forEach(async remote => { - const { name, url } = remote; - // setting getModule for backwards compatibility with parts of stripes that call it.. - loaderArray.push(loadRemoteComponent(url, name)); - }); - await Promise.all(loaderArray); - remotes.forEach((remote, i) => { - const { actsAs, name, url, ...rest } = remote; - const getModule = () => loaderArray[i].default; - actsAs.forEach(type => modules[type].push({ actsAs, name, url, getModule, ...rest })); - }); - } catch (e) { - console.error('Error parsing modules from registry', e); - } - - return modules; -}; const preloadModules = async (remotes) => { const modules = { app: [], plugin: [], settings: [], handler: [] }; - // TODO finish prefetching modules here.... try { const loaderArray = []; remotes.forEach(async remote => { const { name, url } = remote; - // setting getModule for backwards compatibility with parts of stripes that call it.. - loaderArray.push(loadRemoteComponent(url, name)); + loaderArray.push(loadRemoteComponent(url, name) + .then((module) => { + remote.getModule = () => module.default; + })); }); await Promise.all(loaderArray); - remotes.forEach((remote, i) => { - const { actsAs, name, url, ...rest } = remote; - const getModule = () => loaderArray[i].default; - actsAs.forEach(type => modules[type].push({ actsAs, name, url, getModule, ...rest })); + remotes.forEach((remote) => { + const { actsAs } = remote; + actsAs.forEach(type => modules[type].push({ ...remote })); }); } catch (e) { console.error('Error parsing modules from registry', e); @@ -155,11 +133,7 @@ const loadModuleAssets = (stripes, module) => { return loadTranslations(stripes, module) .then((tx) => { // 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. + // because updating store is async and we don't have the updated values quite yet... // // when translations are compiled, the value of the tx[module.displayName] is an array // containing a single object with shape { type: 'messageFormatPattern', value: 'the actual string' } @@ -186,19 +160,11 @@ const loadModuleAssets = (stripes, module) => { }; /** - * 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. + * loadAllModuleAssets + * Loads icons, translations, and sounds for all modules. Inserts the correct 'displayName' for each module. * @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))), -}); - const loadAllModuleAssets = async (stripes, remotes) => { return Promise.all(remotes.map((r) => loadModuleAssets(stripes, r))); }; @@ -222,35 +188,17 @@ const RegistryLoader = ({ stripes, children }) => { // to an array shaped like [ { name: key1, ...app1 }, { name: key2, ...app2 } ...] const remotes = Object.entries(registry.remotes).map(([name, metadata]) => ({ name, ...metadata })); - // load module assets, then load modules... + // load module assets (translations, icons), then load modules... const remotesWithLoadedAssets = await loadAllModuleAssets(stripes, remotes); - // const parsedModules = await loadModules({ stripes, ...parseModules(remotes) }); - const parsedModules = await preloadModules(remotesWithLoadedAssets); - // 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); - // module.getModule = () => component?.default; - // })); - // } - - // preload all modules... - for (const type in parsedModules) { - if (parsedModules[type]) { - parsedModules[type].forEach(async (module) => { - const loadedModule = await loadRemoteComponent(module.url, module.name); - module.getModule = () => loadedModule?.default; - }); - } - } + // load module code - this loads each module only once and up `getModule` so that it can be used sychronously. + const cachedModules = await preloadModules(remotesWithLoadedAssets); // prefetch - setModules(parsedModules); + setModules(cachedModules); }; fetchRegistry(); - // We know what we are doing here so just ignore the dependency warning about 'formatMessage' + // no, we don't want to refetch the registry if stripes changes // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 7dc0e1d22e76593b17aba2de465bbdc143bfe890 Mon Sep 17 00:00:00 2001 From: John Coburn Date: Wed, 19 Nov 2025 08:25:53 -0600 Subject: [PATCH 18/18] only import/re-export context/initial values from shared-context --- src/ModulesContext.js | 6 +++++- src/components/MainNav/AppOrderProvider.js | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ModulesContext.js b/src/ModulesContext.js index 0c5b57d15..2de5ea159 100644 --- a/src/ModulesContext.js +++ b/src/ModulesContext.js @@ -1 +1,5 @@ -export { ModulesContext, modulesInitialState, useModules } from '@folio/stripes-shared-context'; +import { useContext } from 'react'; +import { ModulesContext } from '@folio/stripes-shared-context'; + +export const useModules = () => useContext(ModulesContext); +export { ModulesContext, modulesInitialValue } from '@folio/stripes-shared-context'; diff --git a/src/components/MainNav/AppOrderProvider.js b/src/components/MainNav/AppOrderProvider.js index 8eeaa6bfe..8116d110a 100644 --- a/src/components/MainNav/AppOrderProvider.js +++ b/src/components/MainNav/AppOrderProvider.js @@ -4,7 +4,9 @@ import { useIntl } from 'react-intl'; import { useQuery } from 'react-query'; import isArray from 'lodash/isArray'; -import { LastVisitedContext, useModules, useStripes } from '@folio/stripes-shared-context'; +import { LastVisitedContext } from '@folio/stripes-shared-context'; +import { useStripes } from '../../StripesContext'; +import { useModules } from '../../ModulesContext'; import usePreferences from '../../hooks/usePreferences'; import { packageName } from '../../constants'; import settingsIcon from './settings.svg';