diff --git a/bootstrap.js b/bootstrap.js new file mode 100644 index 000000000..bdb96b828 --- /dev/null +++ b/bootstrap.js @@ -0,0 +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'; + +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/index.js b/index.js index 8eeb5d2cc..f9f1ea443 100644 --- a/index.js +++ b/index.js @@ -57,7 +57,6 @@ export { tenantLocaleConfig } from './src/loginServices'; export { getFullLocale } 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/package.json b/package.json index 80bdf610f..0108dbd62 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,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", 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/AppRoutes.js b/src/AppRoutes.js index f24755a57..da24cbbdb 100644 --- a/src/AppRoutes.js +++ b/src/AppRoutes.js @@ -1,4 +1,4 @@ -import { Suspense, useMemo } from 'react'; +import React, { useMemo, Suspense } from 'react'; import { Route } from 'react-router-dom'; import PropTypes from 'prop-types'; @@ -12,6 +12,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 }) => { @@ -23,11 +24,13 @@ 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); @@ -48,39 +51,41 @@ 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/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 d113f8343..2de5ea159 100644 --- a/src/ModulesContext.js +++ b/src/ModulesContext.js @@ -1,12 +1,5 @@ -import React, { useContext } from 'react'; +import { useContext } from 'react'; +import { ModulesContext } from '@folio/stripes-shared-context'; -export const modulesInitialState = { - app: [], - handler: [], - plugin: [], - settings: [], -}; - -export const ModulesContext = React.createContext(modulesInitialState); -export default ModulesContext; export const useModules = () => useContext(ModulesContext); +export { ModulesContext, modulesInitialValue } from '@folio/stripes-shared-context'; diff --git a/src/RootWithIntl.js b/src/RootWithIntl.js index 4c0339b2b..f814fb511 100644 --- a/src/RootWithIntl.js +++ b/src/RootWithIntl.js @@ -18,7 +18,6 @@ import { MainContainer, MainNav, ModuleContainer, - ModuleTranslator, TitledRoute, Front, OIDCRedirect, @@ -43,6 +42,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); @@ -71,7 +71,7 @@ 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/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/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/AppOrderProvider.js b/src/components/MainNav/AppOrderProvider.js index 4b790da10..8116d110a 100644 --- a/src/components/MainNav/AppOrderProvider.js +++ b/src/components/MainNav/AppOrderProvider.js @@ -2,10 +2,11 @@ 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 { LastVisitedContext } from '@folio/stripes-shared-context'; import { useStripes } from '../../StripesContext'; import { useModules } from '../../ModulesContext'; -import { LastVisitedContext } from '../LastVisited'; import usePreferences from '../../hooks/usePreferences'; import { packageName } from '../../constants'; import settingsIcon from './settings.svg'; @@ -39,11 +40,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 +104,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)); } 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/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..c0426e5bd --- /dev/null +++ b/src/components/RegistryLoader.js @@ -0,0 +1,222 @@ +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { okapi } from 'stripes-config'; + +import { ModulesContext } from '../ModulesContext'; +import loadRemoteComponent from '../loadRemoteComponent'; + +/** + * 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. + * + * @param {array} remotes + * @returns {app: [], plugin: [], settings: [], handler: []} + */ + +const preloadModules = async (remotes) => { + const modules = { app: [], plugin: [], settings: [], handler: [] }; + + try { + const loaderArray = []; + remotes.forEach(async remote => { + const { name, url } = remote; + loaderArray.push(loadRemoteComponent(url, name) + .then((module) => { + remote.getModule = () => module.default; + })); + }); + await Promise.all(loaderArray); + remotes.forEach((remote) => { + const { actsAs } = remote; + actsAs.forEach(type => modules[type].push({ ...remote })); + }); + } catch (e) { + console.error('Error parsing modules from registry', e); + } + + return modules; +}; + +/** + * loadTranslations + * 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 + * + * @returns {Promise} + */ +const loadTranslations = (stripes, module) => { + // 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]; + // }); + + const tx = { ...stripes.okapi.translations, ...translations }; + + // stripes.store.dispatch(setTranslations(tx)); + + // const tx = { ...stripes.okapi.translations, ...keyed }; + // console.log(`filters.status.active: ${tx['ui-users.filters.status.active']}`) + stripes.setLocale(stripes.locale, tx); + return 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 => { + stripes.logger.log('core', ` > ${i.name}`); + + const icon = { + [i.name]: { + src: `${module.host}:${module.port}/icons/${i.name}.svg`, + alt: i.title, + } + }; + 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) => { + // tx[module.displayName] instead of formatMessage({ id: module.displayName}) + // 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' } + // 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, + displayName: module.displayName ? + newDisplayName : module.module, + }; + }) + .catch(e => { + // eslint-disable-next-line no-console + console.error(e); + }); +}; + +/** + * loadAllModuleAssets + * Loads icons, translations, and sounds for all modules. Inserts the correct 'displayName' for each module. + * @param {props} + * @returns Promise + */ +const loadAllModuleAssets = async (stripes, remotes) => { + return Promise.all(remotes.map((r) => loadModuleAssets(stripes, r))); +}; + +/** + * Registry Loader + * @param {object} stripes + * @param {*} children + * @returns + */ +const RegistryLoader = ({ stripes, children }) => { + const [modules, setModules] = useState(); + + // read the list of registered apps from the registry, + useEffect(() => { + const fetchRegistry = async () => { + // 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 })); + + // load module assets (translations, icons), then load modules... + const remotesWithLoadedAssets = await loadAllModuleAssets(stripes, remotes); + // 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(cachedModules); + }; + + fetchRegistry(); + // no, we don't want to refetch the registry if stripes changes + // 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, + ]), + stripes: PropTypes.object.isRequired, +}; + + +export default RegistryLoader; diff --git a/src/components/Root/Root.js b/src/components/Root/Root.js index d28f5151b..dd3e9d66b 100644 --- a/src/components/Root/Root.js +++ b/src/components/Root/Root.js @@ -9,14 +9,13 @@ 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'; 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'; @@ -28,11 +27,6 @@ import './Root.css'; 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); @@ -136,7 +130,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?
; @@ -169,9 +163,9 @@ class Root extends Component { locale, timezone, currency, - metadata, icons, - setLocale: (localeValue) => { loadTranslations(store, localeValue, defaultTranslations); }, + 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)); }, @@ -191,7 +185,7 @@ class Root extends Component { - + { '/bl-users/login-with-expiry', '/bl-users/_self', '/users-keycloak/_self', + '/registry' ]; return !!permissible.find(i => string.startsWith(`${oUrl}${i}`)); 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/components/index.js b/src/components/index.js index 3d6fc3547..639dcecf1 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -13,10 +13,8 @@ export { default as QueryStateUpdater } from './MainNav/QueryStateUpdater'; export { AppOrderProvider } from './MainNav/AppOrderProvider'; 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/gatherActions.js b/src/gatherActions.js index f98e6516b..b794027ef 100644 --- a/src/gatherActions.js +++ b/src/gatherActions.js @@ -14,15 +14,6 @@ function addKeys(moduleName, register, list) { export default function gatherActions(modules) { 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); diff --git a/src/init.js b/src/init.js deleted file mode 100644 index ebf891cce..000000000 --- a/src/init.js +++ /dev/null @@ -1,12 +0,0 @@ -import 'core-js/stable'; -import 'regenerator-runtime/runtime'; -import React from 'react'; -import { createRoot } from 'react-dom/client'; - -import App from './App'; - -export default function init() { - const container = document.getElementById('root'); - const root = createRoot(container); - root.render(); -} diff --git a/src/loadRemoteComponent.js b/src/loadRemoteComponent.js new file mode 100644 index 000000000..3d13a064f --- /dev/null +++ b/src/loadRemoteComponent.js @@ -0,0 +1,23 @@ +// https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers +export default async function loadRemoteComponent(remoteUrl, 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'); + + // 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/loginServices.js b/src/loginServices.js index 275ab29de..8c6356216 100644 --- a/src/loginServices.js +++ b/src/loginServices.js @@ -302,18 +302,18 @@ export async 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. - const res = await 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}`)); } }); - - return res; } /** diff --git a/src/okapiActions.js b/src/okapiActions.js index 6debc86ca..a479cb4bd 100644 --- a/src/okapiActions.js +++ b/src/okapiActions.js @@ -197,7 +197,16 @@ function toggleRtrModal(isVisible) { }; } +function addIcon(key, icon) { + return { + type: OKAPI_REDUCER_ACTIONS.ADD_ICON, + key, + icon + }; +} + export { + addIcon, checkSSO, clearCurrentUser, clearOkapiToken, diff --git a/src/okapiReducer.js b/src/okapiReducer.js index 023e215d0..ecf6a30fb 100644 --- a/src/okapiReducer.js +++ b/src/okapiReducer.js @@ -1,4 +1,5 @@ export const OKAPI_REDUCER_ACTIONS = { + ADD_ICON: 'ADD_ICON', CHECK_SSO: 'CHECK_SSO', CLEAR_CURRENT_USER: 'CLEAR_CURRENT_USER', CLEAR_OKAPI_TOKEN: 'CLEAR_OKAPI_TOKEN', @@ -88,7 +89,7 @@ export default function okapiReducer(state = {}, action) { case OKAPI_REDUCER_ACTIONS.SET_AUTH_FAILURE: return Object.assign({}, state, { authFailure: action.message }); case OKAPI_REDUCER_ACTIONS.SET_TRANSLATIONS: - return Object.assign({}, state, { translations: action.translations }); + return { ...state, translations: { ...state.translations, ...action.translations } }; case OKAPI_REDUCER_ACTIONS.CHECK_SSO: return Object.assign({}, state, { ssoEnabled: action.ssoEnabled }); case OKAPI_REDUCER_ACTIONS.OKAPI_READY: @@ -134,6 +135,34 @@ export default function okapiReducer(state = {}, action) { return { ...state, rtrModalIsVisible: action.isVisible }; } + /** + * 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; } diff --git a/src/translateModules.js b/src/translateModules.js deleted file mode 100644 index e69de29bb..000000000