diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7edfe25..11e0b6478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 13.0.0 IN PROGRESS * Collect coverage from unit tests. Refs UIU-3356. * Use number generator for barcode. Refs UIU-2729. + * Implemented optional loading of Number generator. Refs UIU-2729 ## [12.1.0](https://github.com/folio-org/ui-users/tree/v12.1.0) (2025-03-18) [Full Changelog](https://github.com/folio-org/ui-users/compare/v12.0.0...v12.1.0) diff --git a/src/components/ConditionalLoad/ConditionalLoad.js b/src/components/ConditionalLoad/ConditionalLoad.js new file mode 100644 index 000000000..af832171b --- /dev/null +++ b/src/components/ConditionalLoad/ConditionalLoad.js @@ -0,0 +1,60 @@ +import { lazy, Suspense } from 'react'; +import PropTypes from 'prop-types'; + +import { Loading } from '@folio/stripes/components'; + +// This effectively becomes analogous to Pluggable, except happening at the dependency load stage -- should almost certainly belong to stripes-core +// This protects feature sets such as NumberGenerator, brought in as peer dependencies and therefore present in all platform-core builds, but not necessarily +// in all builds with Users present. +const ConditionalLoad = ({ + children, + FallbackComponent = () =>
Feature not available
, + importString, + importSuccess = m => m, + suppressConsoleErrors = false, + importError = err => { + if (!suppressConsoleErrors) { + console.error('Cannot import, using fallback component', err); + } + return Promise.resolve({ default: FallbackComponent }); + }, + isLocal = false, +}) => { + const dynamicModuleMap = { + '@folio/stripes/components': () => import('@folio/stripes/components'), + '@folio/service-interaction': () => import('@folio/service-interaction'), + }; + + + const Component = lazy(() => { + let importFunc; + if (isLocal) { + importFunc = () => import(importString); + } else { + importFunc = dynamicModuleMap[importString]; + } + + return importFunc() + .then(importSuccess) + .catch(importError); + }); + + + return ( + }> + {children({ Component })} + + ); +}; + +ConditionalLoad.propTypes = { + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + FallbackComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), + importString: PropTypes.string.isRequired, + importSuccess: PropTypes.func, + importError: PropTypes.func, + isLocal: PropTypes.bool, + suppressConsoleErrors: PropTypes.bool, +}; + +export default ConditionalLoad; diff --git a/src/components/ConditionalLoad/ConditionalLoad.test.js b/src/components/ConditionalLoad/ConditionalLoad.test.js new file mode 100644 index 000000000..805330758 --- /dev/null +++ b/src/components/ConditionalLoad/ConditionalLoad.test.js @@ -0,0 +1,112 @@ +import { act, render, waitFor } from '@folio/jest-config-stripes/testing-library/react'; +import ConditionalLoad from './ConditionalLoad'; + +jest.mock('@folio/stripes/components', () => { + return ({ + Test: Promise.resolve({ default: () => <>Testing External }), + Loading: () => <>Loading..., + }); +}); + +let component; +const testConditionalLoad = (props) => async () => { + await act(() => { + component = render( + + {({ Component }) => } + + ); + }); +}; + +describe('ConditionalLoad', () => { + describe('Component renders when import succeeds', () => { + beforeEach(testConditionalLoad({ + importString: './TestComponent', + isLocal: true + })); + + test('renders Test Component', async () => { + const { getByText } = component; + await waitFor(() => { + expect(getByText('Test Component')).toBeInTheDocument(); + }); + }); + }); + + describe('Can get component from module import', () => { + beforeEach(testConditionalLoad({ + importString: './TestComponent/Extra', + importSuccess: m => ({ default: m.Extra }), + isLocal: true + })); + + test('renders Extra Component', async () => { + const { getByText } = component; + await waitFor(() => { + expect(getByText('Extra Component')).toBeInTheDocument(); + }); + }); + }); + + describe('Fallback component renders when import fails', () => { + beforeEach(testConditionalLoad({ + importString: './MadeUpComponent', + isLocal: true + })); + + test('renders Test Component', async () => { + const { getByText } = component; + await waitFor(() => { + expect(getByText('Feature not available')).toBeInTheDocument(); + }); + }); + }); + + describe('Fallback component is configurable', () => { + beforeEach(testConditionalLoad({ + importString: './MadeUpComponent', + FallbackComponent: () =>
Fallback Component
, + isLocal: true + })); + + test('renders Test Component', async () => { + const { getByText } = component; + await waitFor(() => { + expect(getByText('Fallback Component')).toBeInTheDocument(); + }); + }); + }); + + describe('importError is directly configurable', () => { + beforeEach(testConditionalLoad({ + importString: './MadeUpComponent', + importError: (err) => Promise.resolve(({ default: () =>
{err.message}
})), + isLocal: true + })); + + test('renders Test Component', async () => { + const { getByText } = component; + await waitFor(() => { + expect(getByText('Cannot find module \'./MadeUpComponent\' from \'src/components/ConditionalLoad/ConditionalLoad.js\'')).toBeInTheDocument(); + }); + }); + }); + + describe('external import works as expected', () => { + beforeEach(testConditionalLoad({ + importString: '@folio/stripes/components', + importSuccess: m => m.Test, + })); + + test('renders mocked Test Component', async () => { + const { getByText } = component; + await waitFor(() => { + expect(getByText('Testing External')).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/src/components/ConditionalLoad/TestComponent/Extra/Extra.js b/src/components/ConditionalLoad/TestComponent/Extra/Extra.js new file mode 100644 index 000000000..23c77d860 --- /dev/null +++ b/src/components/ConditionalLoad/TestComponent/Extra/Extra.js @@ -0,0 +1 @@ +export default () => (
Extra Component
); diff --git a/src/components/ConditionalLoad/TestComponent/Extra/index.js b/src/components/ConditionalLoad/TestComponent/Extra/index.js new file mode 100644 index 000000000..19d40023e --- /dev/null +++ b/src/components/ConditionalLoad/TestComponent/Extra/index.js @@ -0,0 +1 @@ +export { default as Extra } from './Extra'; diff --git a/src/components/ConditionalLoad/TestComponent/TestComponent.js b/src/components/ConditionalLoad/TestComponent/TestComponent.js new file mode 100644 index 000000000..69158edd7 --- /dev/null +++ b/src/components/ConditionalLoad/TestComponent/TestComponent.js @@ -0,0 +1 @@ +export default () => (
Test Component
); diff --git a/src/components/ConditionalLoad/TestComponent/TestComponent.test.js b/src/components/ConditionalLoad/TestComponent/TestComponent.test.js new file mode 100644 index 000000000..74b1f1e59 --- /dev/null +++ b/src/components/ConditionalLoad/TestComponent/TestComponent.test.js @@ -0,0 +1,26 @@ +import { render } from '@folio/jest-config-stripes/testing-library/react'; +import TestComponent from './TestComponent'; +import { Extra } from './Extra'; + +describe('TestComponent', () => { + let component; + describe('', () => { + beforeEach(() => { + component = render(); + }); + test('should render test component', () => { + const { getByText } = component; + expect(getByText('Test Component')).toBeInTheDocument(); + }); + }); + + describe('', () => { + beforeEach(() => { + component = render(); + }); + test('should render extra component', () => { + const { getByText } = component; + expect(getByText('Extra Component')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/ConditionalLoad/TestComponent/index.js b/src/components/ConditionalLoad/TestComponent/index.js new file mode 100644 index 000000000..3403ebc24 --- /dev/null +++ b/src/components/ConditionalLoad/TestComponent/index.js @@ -0,0 +1 @@ +export { default } from './TestComponent'; diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.js b/src/components/EditSections/EditUserInfo/EditUserInfo.js index d3f9b33fd..260bc38dc 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.js @@ -6,7 +6,6 @@ import { Field } from 'react-final-form'; import { OnChange } from 'react-final-form-listeners'; import { FormattedMessage, injectIntl } from 'react-intl'; -import { NumberGeneratorModalButton } from '@folio/service-interaction'; import { Button, Select, @@ -28,6 +27,8 @@ import validateMinDate from '../../validators/validateMinDate'; import { ChangeUserTypeModal, EditUserProfilePicture } from './components'; +import ConditionalLoad from '../../ConditionalLoad/ConditionalLoad'; + import css from './EditUserInfo.css'; import { validateLength } from '../../validators/validateLength'; import { @@ -388,18 +389,28 @@ class EditUserInfo extends React.Component { fullWidth disabled={disabled || isBarcodeDisabled} /> - {showNumberGeneratorForBarcode && - } - callback={(generated) => form.change('barcode', generated)} - id="userbarcode" - generateButtonLabel={} - generator={BARCODE_GENERATOR_CODE} - modalProps={{ - label: - }} - /> - } + ({ default: m.NumberGeneratorModalButton })} + > + {({ Component: NumberGeneratorModalButton }) => { + if (showNumberGeneratorForBarcode) { + return ( + } + callback={(generated) => form.change('barcode', generated)} + id="userbarcode" + generateButtonLabel={} + generator={BARCODE_GENERATOR_CODE} + modalProps={{ + label: + }} + /> + ); + } + return null; + }} + diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js index e33b40350..32341cca3 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js @@ -5,6 +5,8 @@ import { Form } from 'react-final-form'; import '__mock__/stripesComponents.mock'; +import { NumberGeneratorModalButton as MockNGMB } from '@folio/service-interaction'; + import renderWithRouter from 'helpers/renderWithRouter'; import EditUserInfo from './EditUserInfo'; @@ -27,6 +29,20 @@ jest.mock('@folio/service-interaction', () => ({ ), })); +jest.mock('../../ConditionalLoad/ConditionalLoad', () => ({ + children, + importString, + importSuccess +}) => { + const theImport = jest.requireMock(importString); + const Component = importSuccess(theImport).default; // Little bit hacky but it does the job + return ( + <> + {children({ Component })} + + ); +}); + jest.mock('@folio/stripes/components', () => ({ ...jest.requireActual('@folio/stripes/components'), Modal: jest.fn(({ children, label, footer, ...rest }) => { @@ -266,7 +282,6 @@ describe('Render Edit User Information component', () => { ...props, numberGeneratorData: { barcode: NUMBER_GENERATOR_OPTIONS_OFF }, }); - expect(screen.getByRole('textbox', { name: 'ui-users.information.barcode' })).toBeEnabled(); expect(screen.queryByRole('button', { name: 'NumberGeneratorModalButton' })).not.toBeInTheDocument(); });