Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions src/components/ConditionalLoad/ConditionalLoad.js
Original file line number Diff line number Diff line change
@@ -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 = () => <div>Feature not available</div>,
importString,
importSuccess = m => m,
suppressConsoleErrors = false,
importError = err => {
if (!suppressConsoleErrors) {
console.error('Cannot import, using fallback component', err);

Check warning on line 17 in src/components/ConditionalLoad/ConditionalLoad.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

Unexpected console statement
}
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 (
<Suspense fallback={<Loading />}>
{children({ Component })}
</Suspense>
);
};

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;
112 changes: 112 additions & 0 deletions src/components/ConditionalLoad/ConditionalLoad.test.js
Original file line number Diff line number Diff line change
@@ -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(
<ConditionalLoad
suppressConsoleErrors // We don't need messy errors in the test
{...props}
>
{({ Component }) => <Component />}
</ConditionalLoad>
);
});
};

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: () => <div>Fallback Component</div>,
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: () => <div>{err.message}</div> })),
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();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => (<div>Extra Component</div>);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Extra } from './Extra';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => (<div>Test Component</div>);
26 changes: 26 additions & 0 deletions src/components/ConditionalLoad/TestComponent/TestComponent.test.js
Original file line number Diff line number Diff line change
@@ -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('<TestComponent />', () => {
beforeEach(() => {
component = render(<TestComponent />);
});
test('should render test component', () => {
const { getByText } = component;
expect(getByText('Test Component')).toBeInTheDocument();
});
});

describe('<Extra />', () => {
beforeEach(() => {
component = render(<Extra />);
});
test('should render extra component', () => {
const { getByText } = component;
expect(getByText('Extra Component')).toBeInTheDocument();
});
});
});
1 change: 1 addition & 0 deletions src/components/ConditionalLoad/TestComponent/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './TestComponent';
37 changes: 24 additions & 13 deletions src/components/EditSections/EditUserInfo/EditUserInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -388,18 +389,28 @@ class EditUserInfo extends React.Component {
fullWidth
disabled={disabled || isBarcodeDisabled}
/>
{showNumberGeneratorForBarcode &&
<NumberGeneratorModalButton
buttonLabel={<FormattedMessage id="ui-users.numberGenerator.generateBarcode" />}
callback={(generated) => form.change('barcode', generated)}
id="userbarcode"
generateButtonLabel={<FormattedMessage id="ui-users.numberGenerator.generateBarcode" />}
generator={BARCODE_GENERATOR_CODE}
modalProps={{
label: <FormattedMessage id="ui-users.numberGenerator.barcodeGenerator" />
}}
/>
}
<ConditionalLoad
importString="@folio/service-interaction"
importSuccess={m => ({ default: m.NumberGeneratorModalButton })}
>
{({ Component: NumberGeneratorModalButton }) => {
if (showNumberGeneratorForBarcode) {
return (
<NumberGeneratorModalButton
buttonLabel={<FormattedMessage id="ui-users.numberGenerator.generateBarcode"/>}
callback={(generated) => form.change('barcode', generated)}
id="userbarcode"
generateButtonLabel={<FormattedMessage id="ui-users.numberGenerator.generateBarcode"/>}
generator={BARCODE_GENERATOR_CODE}
modalProps={{
label: <FormattedMessage id="ui-users.numberGenerator.barcodeGenerator"/>
}}
/>
);
}
return null;
}}
</ConditionalLoad>
</Col>
</Row>
</Col>
Expand Down
17 changes: 16 additions & 1 deletion src/components/EditSections/EditUserInfo/EditUserInfo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import '__mock__/stripesComponents.mock';

import { NumberGeneratorModalButton as MockNGMB } from '@folio/service-interaction';

Check warning on line 8 in src/components/EditSections/EditUserInfo/EditUserInfo.test.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'MockNGMB' is defined but never used. Allowed unused vars must match /React/u

import renderWithRouter from 'helpers/renderWithRouter';

import EditUserInfo from './EditUserInfo';
Expand All @@ -27,6 +29,20 @@
),
}));

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 }) => {
Expand Down Expand Up @@ -266,7 +282,6 @@
...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();
});
Expand Down
Loading