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();
});