diff --git a/CHANGELOG.md b/CHANGELOG.md
index afe79524..153ae1b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,9 +17,11 @@
* Add `mod-settings.global.read.circulation` permission. Refs UIREQ-1170.
* Add `mod-settings.entries.collection.get` permission. Refs UIREQ-1177.
* *BREAKING* Migrate to new `mod-circulation-bff` endpoints. Refs UIREQ-1134.
+* Implement feature toggle for ECS and not ECS envs. Refs UIREQ-1171.
## [10.0.1] (https://github.com/folio-org/ui-requests/tree/v10.0.1) (2024-11-13)
[Full Changelog](https://github.com/folio-org/ui-requests/compare/v10.0.0...v10.0.1)
+
* Fix DOMPurify import. Refs UIREQ-1180.
## [10.0.0] (https://github.com/folio-org/ui-requests/tree/v10.0.0) (2024-10-31)
diff --git a/package.json b/package.json
index 1bd5cbba..6e4797e0 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"subPermissions": [
"circulation.requests.queue.collection.get",
"circulation.requests.queue.reorder.collection.post",
+ "circulation.requests.allowed-service-points.get",
"circulation-bff.requests.allowed-service-points.get",
"circulation.rules.request-policy.get"
]
@@ -122,6 +123,8 @@
"subPermissions": [
"ui-requests.view",
"automated-patron-blocks.collection.get",
+ "circulation.requests.item.post",
+ "circulation.requests.allowed-service-points.get",
"circulation-bff.requests.allowed-service-points.get",
"circulation-storage.requests.item.post",
"circulation-storage.request-preferences.collection.get",
@@ -143,6 +146,7 @@
"circulation.pick-slips.get",
"circulation.search-slips.get",
"circulation.requests.item.put",
+ "circulation.requests.allowed-service-points.get",
"circulation-bff.requests.allowed-service-points.get",
"circulation.print-events-entry.item.post",
"circulation-storage.staff-slips.collection.get",
diff --git a/src/deprecated/components/InstanceInformation/InstanceInformation.css b/src/deprecated/components/InstanceInformation/InstanceInformation.css
new file mode 100644
index 00000000..97c034ab
--- /dev/null
+++ b/src/deprecated/components/InstanceInformation/InstanceInformation.css
@@ -0,0 +1,3 @@
+.enterButton {
+ margin-top: 25px;
+}
diff --git a/src/deprecated/components/InstanceInformation/InstanceInformation.js b/src/deprecated/components/InstanceInformation/InstanceInformation.js
new file mode 100644
index 00000000..c6eafab7
--- /dev/null
+++ b/src/deprecated/components/InstanceInformation/InstanceInformation.js
@@ -0,0 +1,265 @@
+import { Component } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { Field } from 'react-final-form';
+import { isNull } from 'lodash';
+
+import {
+ Button,
+ Col,
+ Icon,
+ Row,
+ TextField,
+} from '@folio/stripes/components';
+import { Pluggable } from '@folio/stripes/core';
+
+import {
+ BASE_SPINNER_PROPS,
+ ENTER_EVENT_KEY,
+ REQUEST_FORM_FIELD_NAMES,
+} from '../../../constants';
+import { TitleInformation } from '../../../components';
+import {
+ isFormEditing,
+ memoizeValidation,
+} from '../../../utils';
+
+import css from './InstanceInformation.css';
+
+export const INSTANCE_SEGMENT_FOR_PLUGIN = 'instances';
+
+class InstanceInformation extends Component {
+ static propTypes = {
+ triggerValidation: PropTypes.func.isRequired,
+ findInstance: PropTypes.func.isRequired,
+ submitting: PropTypes.bool.isRequired,
+ form: PropTypes.object.isRequired,
+ values: PropTypes.object.isRequired,
+ onSetSelectedInstance: PropTypes.func.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ instanceId: PropTypes.string.isRequired,
+ request: PropTypes.object,
+ instanceRequestCount: PropTypes.number,
+ selectedInstance: PropTypes.object,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ shouldValidateId: false,
+ isInstanceClicked: false,
+ isInstanceBlurred: false,
+ validatedId: null,
+ };
+ }
+
+ validate = memoizeValidation(async (instanceId) => {
+ const {
+ selectedInstance,
+ findInstance,
+ } = this.props;
+ const { shouldValidateId } = this.state;
+
+ if (!instanceId || (!instanceId && !selectedInstance?.id)) {
+ return ;
+ }
+
+ if (instanceId && shouldValidateId) {
+ this.setState({ shouldValidateId: false });
+
+ const instance = await findInstance(instanceId, null, true);
+
+ return !instance
+ ?
+ : undefined;
+ }
+
+ return undefined;
+ });
+
+ handleChange = (event) => {
+ const { form } = this.props;
+ const {
+ isInstanceClicked,
+ isInstanceBlurred,
+ validatedId,
+ } = this.state;
+ const instanceId = event.target.value;
+
+ if (isInstanceClicked || isInstanceBlurred) {
+ this.setState({
+ isInstanceClicked: false,
+ isInstanceBlurred: false,
+ });
+ }
+
+ if (!isNull(validatedId)) {
+ this.setState({ validatedId: null });
+ }
+
+ form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instanceId);
+ };
+
+ handleBlur = (input) => () => {
+ const { triggerValidation } = this.props;
+ const { validatedId } = this.state;
+
+ if (input.value && input.value !== validatedId) {
+ this.setState({
+ shouldValidateId: true,
+ isInstanceBlurred: true,
+ validatedId: input.value,
+ }, () => {
+ input.onBlur();
+ triggerValidation();
+ });
+ } else if (!input.value) {
+ input.onBlur();
+ }
+ };
+
+ handleClick = (eventKey) => {
+ const {
+ values,
+ onSetSelectedInstance,
+ findInstance,
+ triggerValidation,
+ } = this.props;
+ const instanceId = values.instance?.hrid;
+
+ if (instanceId) {
+ onSetSelectedInstance(null);
+ this.setState(({
+ isInstanceClicked: true,
+ }));
+ findInstance(instanceId);
+
+ if (eventKey === ENTER_EVENT_KEY) {
+ this.setState({
+ shouldValidateId: true,
+ }, triggerValidation);
+ }
+ }
+ };
+
+ onKeyDown = (e) => {
+ if (e.key === ENTER_EVENT_KEY && !e.shiftKey) {
+ e.preventDefault();
+ this.handleClick(e.key);
+ }
+ };
+
+ render() {
+ const {
+ request,
+ selectedInstance,
+ findInstance,
+ submitting,
+ values,
+ isLoading,
+ instanceRequestCount,
+ instanceId,
+ } = this.props;
+ const {
+ isInstanceClicked,
+ isInstanceBlurred,
+ } = this.state;
+ const isEditForm = isFormEditing(request);
+ const titleLevelRequestsCount = request?.titleRequestCount || instanceRequestCount;
+ const isTitleInfoVisible = selectedInstance && !isLoading;
+
+ return (
+
+
+ {
+ !isEditForm &&
+ <>
+
+
+
+ {placeholder => {
+ const key = values.keyOfInstanceIdField ?? 0;
+
+ return (
+
+ {({ input, meta }) => {
+ const selectInstanceError = meta.touched && meta.error;
+ const instanceDoesntExistError = (isInstanceClicked || isInstanceBlurred) && meta.error;
+ const error = selectInstanceError || instanceDoesntExistError || null;
+
+ return (
+ }
+ error={error}
+ onChange={this.handleChange}
+ onBlur={this.handleBlur(input)}
+ onKeyDown={this.onKeyDown}
+ />
+ );
+ }}
+
+ );
+ }}
+
+
+
+
+
+
+
+
+ }
+ selectInstance={(instanceFromPlugin) => findInstance(instanceFromPlugin.hrid)}
+ config={{
+ availableSegments: [{
+ name: INSTANCE_SEGMENT_FOR_PLUGIN,
+ }],
+ }}
+ />
+
+
+ >
+ }
+ {
+ isLoading &&
+ }
+ {
+ isTitleInfoVisible &&
+
+ }
+
+
+ );
+ }
+}
+
+export default InstanceInformation;
diff --git a/src/deprecated/components/InstanceInformation/InstanceInformation.test.js b/src/deprecated/components/InstanceInformation/InstanceInformation.test.js
new file mode 100644
index 00000000..e5fd6abf
--- /dev/null
+++ b/src/deprecated/components/InstanceInformation/InstanceInformation.test.js
@@ -0,0 +1,671 @@
+import { useState } from 'react';
+import { Field } from 'react-final-form';
+
+import {
+ render,
+ screen,
+ fireEvent,
+ cleanup,
+ waitFor,
+} from '@folio/jest-config-stripes/testing-library/react';
+
+import {
+ Icon,
+ TextField,
+} from '@folio/stripes/components';
+import { Pluggable } from '@folio/stripes/core';
+
+import InstanceInformation, {
+ INSTANCE_SEGMENT_FOR_PLUGIN,
+} from './InstanceInformation';
+import { TitleInformation } from '../../../components';
+import { isFormEditing } from '../../../utils';
+import {
+ BASE_SPINNER_PROPS,
+ ENTER_EVENT_KEY,
+ REQUEST_FORM_FIELD_NAMES,
+} from '../../../constants';
+
+jest.mock('../../../utils', () => ({
+ memoizeValidation: (fn) => () => fn,
+ isFormEditing: jest.fn(() => false),
+}));
+jest.mock('../../../components', () => ({
+ TitleInformation: jest.fn(() =>
TitleInformation
),
+}));
+
+const basicProps = {
+ triggerValidation: jest.fn(),
+ findInstance: jest.fn(() => null),
+ onSetSelectedInstance: jest.fn(),
+ form: {
+ change: jest.fn(),
+ },
+ values: {
+ instance: {
+ hrid: 'hrid',
+ },
+ keyOfInstanceIdField: 1,
+ },
+ request: {
+ id: 'requestId',
+ },
+ selectedInstance: {
+ title: 'instance title',
+ contributors: {},
+ publication: {},
+ editions: {},
+ identifiers: {},
+ },
+ instanceRequestCount: 1,
+ instanceId: 'instanceId',
+ isLoading: false,
+ submitting: false,
+};
+const labelIds = {
+ scanOrEnterBarcode: 'ui-requests.instance.scanOrEnterBarcode',
+ instanceHrid: 'ui-requests.instance.value',
+ enterButton: 'ui-requests.enter',
+ selectInstanceRequired: 'ui-requests.errors.selectInstanceRequired',
+ instanceUuidOrHridDoesNotExist: 'ui-requests.errors.instanceUuidOrHridDoesNotExist',
+ titleLookupPlugin: 'ui-requests.titleLookupPlugin',
+};
+const testIds = {
+ instanceHridField: 'instanceHridField',
+ errorMessage: 'errorMessage',
+};
+const renderInstanceInfoWithHrid = (onBlur) => {
+ Field.mockImplementation(jest.fn(({
+ children,
+ 'data-testid': testId,
+ validate,
+ }) => {
+ return children({
+ meta: {},
+ input: {
+ validate,
+ 'data-testid': testId,
+ value: 'hrid',
+ onBlur,
+ },
+ });
+ }));
+
+ render(
+
+ );
+};
+
+describe('InstanceInformation', () => {
+ afterEach(() => {
+ basicProps.onSetSelectedInstance.mockClear();
+ Field.mockClear();
+ cleanup();
+ });
+
+ describe('when "isFormEditing" returns false', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render "scanOrEnterBarcode" placeholder', () => {
+ const scanOrEnterBarcodePlaceholder = screen.getByPlaceholderText(labelIds.scanOrEnterBarcode);
+
+ expect(scanOrEnterBarcodePlaceholder).toBeVisible();
+ });
+
+ it('should render instance hrid "Field" with correct props', () => {
+ const expectedProps = {
+ name: REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID,
+ validate: expect.any(Function),
+ validateFields: [],
+ };
+
+ expect(Field).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should render instance hrid label', () => {
+ const instanceHridLabel = screen.getByText(labelIds.instanceHrid);
+
+ expect(instanceHridLabel).toBeVisible();
+ });
+
+ it('should trigger "findInstance" when Enter key is pressed', () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.keyDown(instanceHridField, { key: ENTER_EVENT_KEY });
+
+ expect(basicProps.findInstance).toHaveBeenCalledWith(basicProps.values.instance.hrid);
+ });
+
+ it('should not trigger "findInstance" when Control key is pressed', () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.keyDown(instanceHridField, { key: 'Control' });
+
+ expect(basicProps.findInstance).not.toHaveBeenCalledWith();
+ });
+
+ it('should trigger "form.change" with correct arguments', () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+ const event = {
+ target: {
+ value: 'instanceHrid',
+ },
+ };
+
+ fireEvent.change(instanceHridField, event);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, event.target.value);
+ });
+
+ it('should render "TextField" with correct props', () => {
+ const expectedProps = {
+ required: true,
+ error: null,
+ placeholder: [labelIds.scanOrEnterBarcode],
+ onChange: expect.any(Function),
+ onBlur: expect.any(Function),
+ onKeyDown: expect.any(Function),
+ };
+
+ expect(TextField).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should render "TextField" with validation error', () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+ const enterButton = screen.getByText(labelIds.enterButton);
+ const event = {
+ target: {
+ value: 'instanceHrid',
+ },
+ };
+ const error = 'error';
+
+ Field.mockImplementationOnce(jest.fn(({
+ children,
+ 'data-testid': testId,
+ validate,
+ }) => {
+ return children({
+ meta: {
+ error,
+ touched: true,
+ },
+ input: {
+ validate,
+ 'data-testid': testId,
+ },
+ });
+ }));
+
+ fireEvent.click(enterButton);
+ fireEvent.change(instanceHridField, event);
+
+ expect(TextField).toHaveBeenCalledWith(expect.objectContaining({ error }), {});
+ });
+
+ it('should render "Pluggable" with correct props', () => {
+ const expectedProps = {
+ searchButtonStyle: 'link',
+ type: 'find-instance',
+ selectInstance: expect.any(Function),
+ config: {
+ availableSegments: [{
+ name: INSTANCE_SEGMENT_FOR_PLUGIN,
+ }],
+ },
+ };
+
+ expect(Pluggable).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should render title lookup plugin label', () => {
+ const titleLookupPluginLabel = screen.getByText(labelIds.titleLookupPlugin);
+
+ expect(titleLookupPluginLabel).toBeVisible();
+ });
+
+ it('should trigger "findInstance" with correct argument', () => {
+ const hrid = 'hrid';
+ const searchButtonLabel = 'Search';
+ const searchButton = screen.getByText(searchButtonLabel);
+
+ fireEvent.click(searchButton);
+
+ expect(basicProps.findInstance).toHaveBeenCalledWith(hrid);
+ });
+ });
+
+ describe('when "isFormEditing" returns true', () => {
+ beforeEach(() => {
+ isFormEditing.mockReturnValueOnce(true);
+
+ render(
+
+ );
+ });
+
+ it('should not render "scanOrEnterBarcode" placeholder', () => {
+ const scanOrEnterBarcodePlaceholder = screen.queryByPlaceholderText(labelIds.scanOrEnterBarcode);
+
+ expect(scanOrEnterBarcodePlaceholder).not.toBeInTheDocument();
+ });
+
+ it('should not render instance hrid field', () => {
+ const instanceHridField = screen.queryByTestId(testIds.instanceHridField);
+
+ expect(instanceHridField).not.toBeInTheDocument();
+ });
+
+ it('should not render instance hrid label', () => {
+ const instanceHridLabel = screen.queryByText(labelIds.instanceHrid);
+
+ expect(instanceHridLabel).not.toBeInTheDocument();
+ });
+ });
+
+ describe('handleBlur', () => {
+ const onBlur = jest.fn();
+
+ afterEach(() => {
+ onBlur.mockClear();
+ });
+
+ it('should trigger "input.onBlur" if instance hrid is presented', () => {
+ renderInstanceInfoWithHrid(onBlur);
+
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.click(instanceHridField);
+ fireEvent.blur(instanceHridField);
+
+ expect(onBlur).toHaveBeenCalled();
+ });
+
+ it('should trigger "input.onBlur" if there is no instance hrid', () => {
+ Field.mockImplementation(jest.fn(({
+ children,
+ 'data-testid': testId,
+ validate,
+ }) => {
+ return children({
+ meta: {},
+ input: {
+ validate,
+ 'data-testid': testId,
+ value: '',
+ onBlur,
+ },
+ });
+ }));
+
+ render(
+
+ );
+
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.click(instanceHridField);
+ fireEvent.blur(instanceHridField);
+
+ expect(onBlur).toHaveBeenCalled();
+ });
+
+ it('should not trigger "input.onBlur" if instance hrid was validated previously', () => {
+ renderInstanceInfoWithHrid(onBlur);
+
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ // first input focus
+ fireEvent.click(instanceHridField);
+ fireEvent.blur(instanceHridField);
+ onBlur.mockClear();
+
+ // second input focus after validation of initial value
+ fireEvent.click(instanceHridField);
+ fireEvent.blur(instanceHridField);
+
+ expect(onBlur).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Validation', () => {
+ afterEach(() => {
+ basicProps.findInstance.mockClear();
+ TextField.mockClear();
+ });
+
+ beforeEach(() => {
+ TextField.mockImplementation(({
+ onChange,
+ validate,
+ ...rest
+ }) => {
+ const [error, setError] = useState('');
+ const handleChange = async (e) => {
+ setError(await validate(e.target.value));
+ onChange(e);
+ };
+
+ return (
+
+
+ {error}
+
+ );
+ });
+ });
+
+ describe('when instance hrid is not presented', () => {
+ const event = {
+ target: {
+ value: '',
+ },
+ };
+
+ it('should not render error message', async () => {
+ render(
+
+ );
+
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.change(instanceHridField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.getByTestId(testIds.errorMessage);
+
+ expect(errorMessage).toBeEmpty();
+ });
+ });
+
+ it('should render "selectInstanceRequired" error message', async () => {
+ const props = {
+ ...basicProps,
+ selectedInstance: {
+ id: 'hrid',
+ },
+ };
+
+ render(
+
+ );
+
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.change(instanceHridField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.queryByText(labelIds.selectInstanceRequired);
+
+ expect(errorMessage).toBeVisible();
+ });
+ });
+ });
+
+ describe('when instance hrid is presented', () => {
+ const event = {
+ target: {
+ value: 'instanceId',
+ },
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not render error message', async () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.change(instanceHridField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.getByTestId(testIds.errorMessage);
+
+ expect(errorMessage).toBeEmpty();
+ });
+ });
+
+ it('should render "instanceUuidOrHridDoesNotExist" error message', async () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ fireEvent.keyDown(instanceHridField, { key: ENTER_EVENT_KEY });
+ fireEvent.change(instanceHridField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.queryByText(labelIds.instanceUuidOrHridDoesNotExist);
+
+ expect(errorMessage).toBeVisible();
+ });
+ });
+
+ it('should not render error message if instance found', async () => {
+ const instanceHridField = screen.getByTestId(testIds.instanceHridField);
+
+ basicProps.findInstance.mockReturnValue({});
+ fireEvent.keyDown(instanceHridField, { key: ENTER_EVENT_KEY });
+ fireEvent.change(instanceHridField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.getByTestId(testIds.errorMessage);
+
+ expect(errorMessage).toBeEmpty();
+ });
+ });
+ });
+ });
+
+ describe('"Enter" button', () => {
+ describe('when instance hrid is presented', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render "Enter" button', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ expect(enterButton).toBeVisible();
+ });
+
+ it('should trigger "onSetSelectedInstance" with correct argument', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ fireEvent.click(enterButton);
+
+ expect(basicProps.onSetSelectedInstance).toHaveBeenCalledWith(null);
+ });
+
+ it('should trigger "findInstance" with correct argument', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ fireEvent.click(enterButton);
+
+ expect(basicProps.findInstance).toHaveBeenCalledWith(basicProps.values.instance.hrid);
+ });
+ });
+
+ describe('when instance hrid is not presented', () => {
+ const props = {
+ ...basicProps,
+ values: {
+ instance: {
+ hrid: '',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not trigger "onSetSelectedInstance"', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ fireEvent.click(enterButton);
+
+ expect(basicProps.onSetSelectedInstance).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('Spinner', () => {
+ afterEach(() => {
+ Icon.mockClear();
+ });
+
+ describe('when data is loading', () => {
+ const props = {
+ ...basicProps,
+ isLoading: true,
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render loading "Icon" with correct props', () => {
+ expect(Icon).toHaveBeenCalledWith(BASE_SPINNER_PROPS, {});
+ });
+ });
+
+ describe('when data is not loading', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not render loading "Icon"', () => {
+ expect(Icon).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('TitleInformation', () => {
+ afterEach(() => {
+ TitleInformation.mockClear();
+ });
+
+ describe('when instance is selected', () => {
+ it('should render "TitleInformation" with correct props', () => {
+ render(
+
+ );
+
+ const expectedProps = {
+ instanceId: basicProps.instanceId,
+ titleLevelRequestsCount: basicProps.instanceRequestCount,
+ title: basicProps.selectedInstance.title,
+ contributors: basicProps.selectedInstance.contributors,
+ publications: basicProps.selectedInstance.publication,
+ editions: basicProps.selectedInstance.editions,
+ identifiers: basicProps.selectedInstance.identifiers,
+ };
+
+ expect(TitleInformation).toHaveBeenCalledWith(expectedProps, {});
+ });
+
+ it('should render "TitleInformation" with "request.instanceId"', () => {
+ const instanceId = 'instanceId';
+ const props = {
+ ...basicProps,
+ request: {
+ ...basicProps.request,
+ instanceId,
+ },
+ };
+ const expectedProps = {
+ instanceId,
+ };
+
+ render(
+
+ );
+
+ expect(TitleInformation).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should render "TitleInformation" with "selectedInstance.id"', () => {
+ const selectedInstanceId = 'selectedInstanceId';
+ const props = {
+ ...basicProps,
+ selectedInstance: {
+ ...basicProps.selectedInstance,
+ id: selectedInstanceId,
+ },
+ };
+ const expectedProps = {
+ instanceId: selectedInstanceId,
+ };
+
+ render(
+
+ );
+
+ expect(TitleInformation).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+ });
+
+ describe('when instance is not selected', () => {
+ const props = {
+ ...basicProps,
+ selectedInstance: undefined,
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not render "TitleInformation"', () => {
+ expect(TitleInformation).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/deprecated/components/ItemInformation/ItemInformation.css b/src/deprecated/components/ItemInformation/ItemInformation.css
new file mode 100644
index 00000000..97c034ab
--- /dev/null
+++ b/src/deprecated/components/ItemInformation/ItemInformation.css
@@ -0,0 +1,3 @@
+.enterButton {
+ margin-top: 25px;
+}
diff --git a/src/deprecated/components/ItemInformation/ItemInformation.js b/src/deprecated/components/ItemInformation/ItemInformation.js
new file mode 100644
index 00000000..58e517e3
--- /dev/null
+++ b/src/deprecated/components/ItemInformation/ItemInformation.js
@@ -0,0 +1,250 @@
+import { Component } from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+import { Field } from 'react-final-form';
+import { isNull } from 'lodash';
+
+import {
+ Button,
+ Col,
+ Icon,
+ Row,
+ TextField,
+} from '@folio/stripes/components';
+
+import {
+ REQUEST_FORM_FIELD_NAMES,
+ RESOURCE_KEYS,
+ ENTER_EVENT_KEY,
+ BASE_SPINNER_PROPS,
+} from '../../../constants';
+import ItemDetail from '../../../ItemDetail';
+import {
+ isFormEditing,
+ memoizeValidation,
+} from '../../../utils';
+
+import css from './ItemInformation.css';
+
+class ItemInformation extends Component {
+ static propTypes = {
+ triggerValidation: PropTypes.func.isRequired,
+ findItem: PropTypes.func.isRequired,
+ form: PropTypes.object.isRequired,
+ values: PropTypes.object.isRequired,
+ request: PropTypes.object.isRequired,
+ onSetSelectedItem: PropTypes.func.isRequired,
+ itemRequestCount: PropTypes.number.isRequired,
+ instanceId: PropTypes.string.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ submitting: PropTypes.bool.isRequired,
+ isItemIdRequest: PropTypes.bool.isRequired,
+ selectedLoan: PropTypes.object,
+ selectedItem: PropTypes.object,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ shouldValidateBarcode: false,
+ isItemClicked: false,
+ isItemBlurred: false,
+ validatedBarcode: null,
+ };
+ }
+
+ validate = memoizeValidation(async (barcode) => {
+ const {
+ isItemIdRequest,
+ findItem,
+ } = this.props;
+ const { shouldValidateBarcode } = this.state;
+
+ if (isItemIdRequest && !barcode) {
+ return undefined;
+ }
+
+ if (!barcode && !isItemIdRequest) {
+ return ;
+ }
+
+ if (barcode && shouldValidateBarcode) {
+ this.setState({ shouldValidateBarcode: false });
+
+ const item = await findItem(RESOURCE_KEYS.barcode, barcode, true);
+
+ return !item
+ ?
+ : undefined;
+ }
+
+ return undefined;
+ });
+
+ handleChange = (event) => {
+ const { form } = this.props;
+ const {
+ isItemClicked,
+ isItemBlurred,
+ validatedBarcode,
+ } = this.state;
+ const barcode = event.target.value;
+
+ if (isItemClicked || isItemBlurred) {
+ this.setState({
+ isItemClicked: false,
+ isItemBlurred: false,
+ });
+ }
+
+ if (!isNull(validatedBarcode)) {
+ this.setState({ validatedBarcode: null });
+ }
+
+ form.change(REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, barcode);
+ };
+
+ handleBlur = (input) => () => {
+ const { triggerValidation } = this.props;
+ const { validatedBarcode } = this.state;
+
+ if (input.value && input.value !== validatedBarcode) {
+ this.setState({
+ shouldValidateBarcode: true,
+ isItemBlurred: true,
+ validatedBarcode: input.value,
+ }, () => {
+ input.onBlur();
+ triggerValidation();
+ });
+ } else if (!input.value) {
+ input.onBlur();
+ }
+ }
+
+ onKeyDown = (e) => {
+ if (e.key === ENTER_EVENT_KEY && !e.shiftKey) {
+ e.preventDefault();
+ this.handleClick(e.key);
+ }
+ };
+
+ handleClick = (eventKey) => {
+ const {
+ onSetSelectedItem,
+ findItem,
+ triggerValidation,
+ values,
+ } = this.props;
+ const barcode = values.item?.barcode;
+
+ if (barcode) {
+ onSetSelectedItem(null);
+ this.setState(({
+ isItemClicked: true,
+ }));
+
+ findItem(RESOURCE_KEYS.barcode, barcode);
+
+ if (eventKey === ENTER_EVENT_KEY) {
+ this.setState({
+ shouldValidateBarcode: true,
+ }, triggerValidation);
+ }
+ }
+ }
+
+ render() {
+ const {
+ values,
+ submitting,
+ isLoading,
+ selectedItem,
+ request,
+ instanceId,
+ selectedLoan,
+ itemRequestCount,
+ } = this.props;
+ const {
+ isItemClicked,
+ isItemBlurred,
+ } = this.state;
+ const isEditForm = isFormEditing(request);
+
+ return (
+
+
+ {
+ !isEditForm &&
+
+
+
+ {placeholder => {
+ const key = values.keyOfItemBarcodeField ?? 0;
+
+ return (
+
+ {({ input, meta }) => {
+ const selectItemError = meta.touched && meta.error;
+ const itemDoesntExistError = (isItemClicked || isItemBlurred) && meta.error;
+ const error = meta.submitError || selectItemError || itemDoesntExistError || null;
+
+ return (
+ }
+ error={error}
+ onChange={this.handleChange}
+ onBlur={this.handleBlur(input)}
+ onKeyDown={this.onKeyDown}
+ />
+ );
+ }}
+
+ );
+ }}
+
+
+
+
+
+
+ }
+ {
+ isLoading &&
+ }
+ {
+ selectedItem &&
+
+ }
+
+
+ );
+ }
+}
+
+export default ItemInformation;
diff --git a/src/deprecated/components/ItemInformation/ItemInformation.test.js b/src/deprecated/components/ItemInformation/ItemInformation.test.js
new file mode 100644
index 00000000..5e6a7347
--- /dev/null
+++ b/src/deprecated/components/ItemInformation/ItemInformation.test.js
@@ -0,0 +1,589 @@
+import { useState } from 'react';
+import { Field } from 'react-final-form';
+
+import {
+ render,
+ screen,
+ fireEvent,
+ cleanup,
+ waitFor,
+} from '@folio/jest-config-stripes/testing-library/react';
+import {
+ Icon,
+ TextField,
+} from '@folio/stripes/components';
+
+import ItemInformation from './ItemInformation';
+import ItemDetail from '../../../ItemDetail';
+import { isFormEditing } from '../../../utils';
+import {
+ REQUEST_FORM_FIELD_NAMES,
+ RESOURCE_KEYS,
+ ENTER_EVENT_KEY,
+ BASE_SPINNER_PROPS,
+} from '../../../constants';
+
+jest.mock('../../../utils', () => ({
+ isFormEditing: jest.fn(() => false),
+ memoizeValidation: (fn) => () => fn,
+}));
+jest.mock('../../../ItemDetail', () => jest.fn(() => Item Details
));
+
+const basicProps = {
+ triggerValidation: jest.fn(),
+ findItem: jest.fn(() => null),
+ onSetSelectedItem: jest.fn(),
+ form: {
+ change: jest.fn(),
+ },
+ values: {
+ item: {
+ barcode: 'itemBarcode',
+ },
+ keyOfItemBarcodeField: 1,
+ },
+ request: {
+ id: 'requestId',
+ },
+ selectedLoan: {},
+ selectedItem: {},
+ itemRequestCount: 1,
+ instanceId: 'instanceId',
+ isLoading: false,
+ submitting: false,
+ isItemIdRequest: true,
+};
+const labelIds = {
+ scanOrEnterBarcode: 'ui-requests.item.scanOrEnterBarcode',
+ itemBarcode: 'ui-requests.item.barcode',
+ enterButton: 'ui-requests.enter',
+ selectItemRequired: 'ui-requests.errors.selectItemRequired',
+ itemBarcodeDoesNotExist: 'ui-requests.errors.itemBarcodeDoesNotExist',
+};
+const testIds = {
+ itemBarcodeField: 'itemBarcodeField',
+ errorMessage: 'errorMessage',
+};
+const renderItemInfoWithBarcode = (onBlur) => {
+ Field.mockImplementation(jest.fn(({
+ children,
+ 'data-testid': testId,
+ validate,
+ }) => {
+ return children({
+ meta: {},
+ input: {
+ validate,
+ 'data-testid': testId,
+ value: 'itemBarcode',
+ onBlur,
+ },
+ });
+ }));
+
+ render(
+
+ );
+};
+
+describe('ItemInformation', () => {
+ afterEach(() => {
+ basicProps.onSetSelectedItem.mockClear();
+ Field.mockClear();
+ cleanup();
+ });
+
+ describe('when "isFormEditing" returns false', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render "scanOrEnterBarcode" placeholder', () => {
+ const scanOrEnterBarcodePlaceholder = screen.getByPlaceholderText(labelIds.scanOrEnterBarcode);
+
+ expect(scanOrEnterBarcodePlaceholder).toBeVisible();
+ });
+
+ it('should render item barcode field', () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ expect(itemBarcodeField).toBeVisible();
+ });
+
+ it('should render item barcode "Field" with correct props', () => {
+ const expectedProps = {
+ name: REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE,
+ validate: expect.any(Function),
+ validateFields: [],
+ };
+
+ expect(Field).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should render item barcode label', () => {
+ const itemBarcodeLabel = screen.getByText(labelIds.itemBarcode);
+
+ expect(itemBarcodeLabel).toBeVisible();
+ });
+
+ it('should trigger "findItem" when Enter key is pressed', () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.keyDown(itemBarcodeField, { key: ENTER_EVENT_KEY });
+
+ expect(basicProps.findItem).toHaveBeenCalledWith(RESOURCE_KEYS.barcode, basicProps.values.item.barcode);
+ });
+
+ it('should not trigger "findItem" when Control key is pressed', () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.keyDown(itemBarcodeField, { key: 'Control' });
+
+ expect(basicProps.findItem).not.toHaveBeenCalledWith();
+ });
+
+ it('should trigger "form.change" with correct arguments', () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+ const event = {
+ target: {
+ value: 'itemBarcode',
+ },
+ };
+
+ fireEvent.change(itemBarcodeField, event);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, event.target.value);
+ });
+
+ it('should render "TextField" with correct props', () => {
+ const expectedProps = {
+ required: true,
+ error: null,
+ onChange: expect.any(Function),
+ onBlur: expect.any(Function),
+ onKeyDown: expect.any(Function),
+ };
+
+ expect(TextField).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should render "TextField" with validation error', () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+ const enterButton = screen.getByText(labelIds.enterButton);
+ const event = {
+ target: {
+ value: 'itemBarcode',
+ },
+ };
+ const error = 'error';
+
+ Field.mockImplementationOnce(jest.fn(({
+ children,
+ 'data-testid': testId,
+ validate,
+ }) => {
+ return children({
+ meta: {
+ error: 'error',
+ touched: true,
+ },
+ input: {
+ validate,
+ 'data-testid': testId,
+ },
+ });
+ }));
+
+ fireEvent.click(enterButton);
+ fireEvent.change(itemBarcodeField, event);
+
+ expect(TextField).toHaveBeenCalledWith(expect.objectContaining({ error }), {});
+ });
+ });
+
+ describe('when "isFormEditing" returns true', () => {
+ beforeEach(() => {
+ isFormEditing.mockReturnValueOnce(true);
+
+ render(
+
+ );
+ });
+
+ it('should not render "scanOrEnterBarcode" placeholder', () => {
+ const scanOrEnterBarcodePlaceholder = screen.queryByPlaceholderText(labelIds.scanOrEnterBarcode);
+
+ expect(scanOrEnterBarcodePlaceholder).not.toBeInTheDocument();
+ });
+
+ it('should not render item barcode field', () => {
+ const itemBarcodeField = screen.queryByTestId(testIds.itemBarcodeField);
+
+ expect(itemBarcodeField).not.toBeInTheDocument();
+ });
+
+ it('should not render item barcode label', () => {
+ const itemBarcodeLabel = screen.queryByText(labelIds.itemBarcode);
+
+ expect(itemBarcodeLabel).not.toBeInTheDocument();
+ });
+ });
+
+ describe('handleBlur', () => {
+ const onBlur = jest.fn();
+
+ afterEach(() => {
+ onBlur.mockClear();
+ });
+
+ it('should trigger "input.onBlur" if item barcode is presented', () => {
+ renderItemInfoWithBarcode(onBlur);
+
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.click(itemBarcodeField);
+ fireEvent.blur(itemBarcodeField);
+
+ expect(onBlur).toHaveBeenCalled();
+ });
+
+ it('should trigger "input.onBlur" if there is no item barcode', () => {
+ Field.mockImplementation(jest.fn(({
+ children,
+ 'data-testid': testId,
+ validate,
+ }) => {
+ return children({
+ meta: {},
+ input: {
+ validate,
+ 'data-testid': testId,
+ value: '',
+ onBlur,
+ },
+ });
+ }));
+
+ render(
+
+ );
+
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.click(itemBarcodeField);
+ fireEvent.blur(itemBarcodeField);
+
+ expect(onBlur).toHaveBeenCalled();
+ });
+
+ it('should not trigger "input.onBlur" if item barcode was validated previously', () => {
+ renderItemInfoWithBarcode(onBlur);
+
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ // first input focus
+ fireEvent.click(itemBarcodeField);
+ fireEvent.blur(itemBarcodeField);
+ onBlur.mockClear();
+
+ // second input focus after validation of initial value
+ fireEvent.click(itemBarcodeField);
+ fireEvent.blur(itemBarcodeField);
+
+ expect(onBlur).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Validation', () => {
+ afterEach(() => {
+ TextField.mockClear();
+ basicProps.findItem.mockClear();
+ });
+
+ beforeEach(() => {
+ TextField.mockImplementation(jest.fn(({
+ onChange,
+ validate,
+ ...rest
+ }) => {
+ const [error, setError] = useState('');
+ const handleChange = async (e) => {
+ setError(await validate(e.target.value));
+ onChange(e);
+ };
+
+ return (
+
+
+ {error}
+
+ );
+ }));
+ });
+
+ describe('when "barcode" is not presented', () => {
+ const event = {
+ target: {
+ value: '',
+ },
+ };
+
+ it('should not render error message', async () => {
+ render(
+
+ );
+
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.change(itemBarcodeField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.getByTestId(testIds.errorMessage);
+
+ expect(errorMessage).toBeEmpty();
+ });
+ });
+
+ it('should render "selectItemRequired" error message', async () => {
+ const props = {
+ ...basicProps,
+ isItemIdRequest: false,
+ };
+
+ render(
+
+ );
+
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.change(itemBarcodeField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.queryByText(labelIds.selectItemRequired);
+
+ expect(errorMessage).toBeVisible();
+ });
+ });
+ });
+
+ describe('when "barcode" is presented', () => {
+ const event = {
+ target: {
+ value: 'barcode',
+ },
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not render error message', async () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.change(itemBarcodeField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.getByTestId(testIds.errorMessage);
+
+ expect(errorMessage).toBeEmpty();
+ });
+ });
+
+ it('should render "itemBarcodeDoesNotExist" error message', async () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ fireEvent.keyDown(itemBarcodeField, { key: ENTER_EVENT_KEY });
+ fireEvent.change(itemBarcodeField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.queryByText(labelIds.itemBarcodeDoesNotExist);
+
+ expect(errorMessage).toBeVisible();
+ });
+ });
+
+ it('should not render error message if item found', async () => {
+ const itemBarcodeField = screen.getByTestId(testIds.itemBarcodeField);
+
+ basicProps.findItem.mockReturnValue({});
+ fireEvent.keyDown(itemBarcodeField, { key: ENTER_EVENT_KEY });
+ fireEvent.change(itemBarcodeField, event);
+
+ await waitFor(() => {
+ const errorMessage = screen.getByTestId(testIds.errorMessage);
+
+ expect(errorMessage).toBeEmpty();
+ });
+ });
+ });
+ });
+
+ describe('"Enter" button', () => {
+ describe('when barcode is presented', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render "Enter" button', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ expect(enterButton).toBeVisible();
+ });
+
+ it('should trigger "onSetSelectedItem" with correct argument', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ fireEvent.click(enterButton);
+
+ expect(basicProps.onSetSelectedItem).toHaveBeenCalledWith(null);
+ });
+
+ it('should trigger "findItem" with correct arguments', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ fireEvent.click(enterButton);
+
+ expect(basicProps.findItem).toHaveBeenCalledWith(RESOURCE_KEYS.barcode, basicProps.values.item.barcode);
+ });
+ });
+
+ describe('when barcode is not presented', () => {
+ const props = {
+ ...basicProps,
+ values: {
+ item: {
+ barcode: '',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not trigger "onSetSelectedItem"', () => {
+ const enterButton = screen.getByText(labelIds.enterButton);
+
+ fireEvent.click(enterButton);
+
+ expect(basicProps.onSetSelectedItem).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('Spinner', () => {
+ afterEach(() => {
+ Icon.mockClear();
+ });
+
+ describe('when data is loading', () => {
+ const props = {
+ ...basicProps,
+ isLoading: true,
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render loading "Icon" with correct props', () => {
+ expect(Icon).toHaveBeenCalledWith(BASE_SPINNER_PROPS, {});
+ });
+ });
+
+ describe('when data is not loading', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not render loading "Icon"', () => {
+ expect(Icon).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('ItemDetails', () => {
+ afterEach(() => {
+ ItemDetail.mockClear();
+ });
+
+ describe('when item is selected', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render "ItemDetail" with correct props', () => {
+ const expectedProps = {
+ request: basicProps.request,
+ currentInstanceId: basicProps.instanceId,
+ item: basicProps.selectedItem,
+ loan: basicProps.selectedLoan,
+ requestCount: basicProps.itemRequestCount,
+ };
+
+ expect(ItemDetail).toHaveBeenCalledWith(expectedProps, {});
+ });
+ });
+
+ describe('when item is not selected', () => {
+ const props = {
+ ...basicProps,
+ selectedItem: undefined,
+ };
+
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should not render "ItemDetail"', () => {
+ expect(ItemDetail).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/deprecated/components/ItemsDialog/ItemsDialog.css b/src/deprecated/components/ItemsDialog/ItemsDialog.css
new file mode 100644
index 00000000..00fa56d4
--- /dev/null
+++ b/src/deprecated/components/ItemsDialog/ItemsDialog.css
@@ -0,0 +1,4 @@
+.content {
+ padding: 0px;
+ margin: 0px;
+}
diff --git a/src/deprecated/components/ItemsDialog/ItemsDialog.js b/src/deprecated/components/ItemsDialog/ItemsDialog.js
new file mode 100644
index 00000000..054b29ac
--- /dev/null
+++ b/src/deprecated/components/ItemsDialog/ItemsDialog.js
@@ -0,0 +1,283 @@
+import React, {
+ useState,
+ useLayoutEffect,
+ useMemo,
+} from 'react';
+import PropTypes from 'prop-types';
+import {
+ get,
+ noop,
+ countBy,
+ chunk,
+} from 'lodash';
+import {
+ useIntl,
+ FormattedMessage,
+} from 'react-intl';
+
+import {
+ Modal,
+ MultiColumnList,
+ Pane,
+ Paneset,
+} from '@folio/stripes/components';
+import { stripesConnect } from '@folio/stripes/core';
+
+import {
+ itemStatuses,
+ itemStatusesTranslations,
+ requestableItemStatuses,
+ MAX_RECORDS,
+ OPEN_REQUESTS_STATUSES,
+} from '../../../constants';
+import { Loading } from '../../../components';
+import { getStatusQuery } from '../../../routes/utils';
+
+import css from './ItemsDialog.css';
+
+export const COLUMN_NAMES = [
+ 'barcode',
+ 'itemStatus',
+ 'requestQueue',
+ 'location',
+ 'materialType',
+ 'loanType',
+];
+
+export const COLUMN_WIDTHS = {
+ barcode: '16%',
+ itemStatus: '16%',
+ requestQueue: '16%',
+ location: '16%',
+ materialType: '16%',
+ loanType: '16%',
+};
+
+export const COLUMN_MAP = {
+ barcode: ,
+ itemStatus: ,
+ requestQueue: ,
+ location: ,
+ materialType: ,
+ loanType: ,
+};
+
+export const formatter = {
+ itemStatus: item => ,
+ location: item => get(item, 'effectiveLocation.name', ''),
+ materialType: item => item.materialType.name,
+ loanType: item => (item.temporaryLoanType ? get(item, 'temporaryLoanType.name', '') : get(item, 'permanentLoanType.name', '')),
+};
+
+export const MAX_HEIGHT = 500;
+const CHUNK_SIZE = 40;
+
+const ItemsDialog = ({
+ onClose,
+ open,
+ isLoading,
+ onRowClick = noop,
+ mutator,
+ skippedItemId,
+ title,
+ instanceId,
+}) => {
+ const [areItemsBeingLoaded, setAreItemsBeingLoaded] = useState(false);
+ const [items, setItems] = useState([]);
+ const { formatMessage } = useIntl();
+
+ const fetchHoldings = () => {
+ const query = `instanceId==${instanceId}`;
+ mutator.holdings.reset();
+
+ return mutator.holdings.GET({ params: { query, limit: MAX_RECORDS } });
+ };
+
+ const fetchItems = async (holdings) => {
+ const chunkedItems = chunk(holdings, CHUNK_SIZE);
+ const data = [];
+
+ for (const itemChunk of chunkedItems) {
+ const query = itemChunk.map(i => `holdingsRecordId==${i.id}`).join(' or ');
+
+ mutator.items.reset();
+ // eslint-disable-next-line no-await-in-loop
+ const result = await mutator.items.GET({ params: { query, limit: MAX_RECORDS } });
+
+ data.push(...result);
+ }
+
+ return data;
+ };
+
+ const fetchRequests = async (itemsList) => {
+ // Split the list of items into small chunks to create a short enough query string
+ // that we can avoid a "414 Request URI Too Long" response from Okapi.
+ const chunkedItems = chunk(itemsList, CHUNK_SIZE);
+ const data = [];
+
+ for (const itemChunk of chunkedItems) {
+ let query = itemChunk.map(i => `itemId==${i.id}`).join(' or ');
+ const statusQuery = getStatusQuery(OPEN_REQUESTS_STATUSES);
+
+ query = `(${query}) and (${statusQuery})")`;
+
+ mutator.requests.reset();
+ // eslint-disable-next-line no-await-in-loop
+ const result = await mutator.requests.GET({ params: { query, limit: MAX_RECORDS } });
+
+ data.push(...result);
+ }
+
+ return data;
+ };
+
+ useLayoutEffect(() => {
+ const getItems = async () => {
+ setAreItemsBeingLoaded(true);
+
+ const holdings = await fetchHoldings();
+ let itemsList = await fetchItems(holdings);
+
+ if (skippedItemId) {
+ itemsList = itemsList.filter(item => requestableItemStatuses.includes(item.status?.name));
+ }
+
+ const requests = await fetchRequests(itemsList);
+ const requestMap = countBy(requests, 'itemId');
+
+ itemsList = itemsList.map(item => ({ ...item, requestQueue: requestMap[item.id] || 0 }));
+
+ setAreItemsBeingLoaded(false);
+ setItems(itemsList);
+ };
+
+ if (open && instanceId) {
+ getItems();
+ }
+
+ return () => setItems([]);
+ },
+ // The deps react-hooks complains about here are the fetch* functions
+ // but both the suggestions (making them deps, moving them inside this
+ // function) cause test failures. I ... don't really understand the
+ // details of the problem, beyond the fact that it's obviously some kind
+ // of bad interaction between hooks and stripes-connect.
+ //
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [instanceId, open]);
+
+ const contentData = useMemo(() => {
+ let resultItems = items;
+
+ if (skippedItemId) {
+ resultItems = resultItems
+ .filter(item => skippedItemId !== item.id);
+ }
+
+ // items with status available must go first
+ resultItems.sort((a) => (a.status.name === itemStatuses.AVAILABLE ? -1 : 1));
+ return resultItems.map(item => ({
+ ...item,
+ status: {
+ ...item.status,
+ },
+ }));
+ }, [items, skippedItemId]);
+
+ const itemsAmount = contentData.length;
+
+ return (
+
+
+
+ {isLoading || areItemsBeingLoaded
+ ?
+ :
+ }
+
+
+
+ );
+};
+
+ItemsDialog.manifest = {
+ holdings: {
+ type: 'okapi',
+ records: 'holdingsRecords',
+ path: 'holdings-storage/holdings',
+ accumulate: true,
+ fetch: false,
+ },
+ items: {
+ type: 'okapi',
+ records: 'items',
+ path: 'inventory/items',
+ accumulate: true,
+ fetch: false,
+ },
+ requests: {
+ type: 'okapi',
+ path: 'circulation/requests',
+ records: 'requests',
+ accumulate: true,
+ fetch: false,
+ },
+};
+
+ItemsDialog.defaultProps = {
+ title: '',
+};
+
+ItemsDialog.propTypes = {
+ open: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ isLoading: PropTypes.bool,
+ title: PropTypes.string,
+ instanceId: PropTypes.string,
+ skippedItemId: PropTypes.string,
+ onRowClick: PropTypes.func,
+ mutator: PropTypes.shape({
+ holdings: PropTypes.shape({
+ GET: PropTypes.func.isRequired,
+ reset: PropTypes.func.isRequired,
+ }).isRequired,
+ items: PropTypes.shape({
+ GET: PropTypes.func.isRequired,
+ reset: PropTypes.func.isRequired,
+ }).isRequired,
+ requests: PropTypes.shape({
+ GET: PropTypes.func.isRequired,
+ reset: PropTypes.func.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
+export default stripesConnect(ItemsDialog);
diff --git a/src/deprecated/components/ItemsDialog/ItemsDialog.test.js b/src/deprecated/components/ItemsDialog/ItemsDialog.test.js
new file mode 100644
index 00000000..ea2f2083
--- /dev/null
+++ b/src/deprecated/components/ItemsDialog/ItemsDialog.test.js
@@ -0,0 +1,490 @@
+import {
+ render,
+ screen,
+} from '@folio/jest-config-stripes/testing-library/react';
+
+import {
+ Modal,
+ MultiColumnList,
+ Pane,
+ Paneset,
+} from '@folio/stripes/components';
+
+import {
+ itemStatuses,
+ requestableItemStatuses,
+} from '../../../constants';
+import { Loading } from '../../../components';
+import ItemsDialog, {
+ COLUMN_NAMES,
+ COLUMN_WIDTHS,
+ COLUMN_MAP,
+ formatter,
+ MAX_HEIGHT,
+} from './ItemsDialog';
+
+jest.mock('../../../components', () => ({
+ Loading: jest.fn((props) => (
+
+ )),
+}));
+
+const testIds = {
+ loading: 'loading',
+};
+const labelIds = {
+ selectItem: 'ui-requests.items.selectItem',
+ instanceItems: 'ui-requests.items.instanceItems',
+ resultCount: 'ui-requests.resultCount',
+ instanceItemsNotFound: 'ui-requests.items.instanceItems.notFound',
+};
+
+describe('ItemsDialog', () => {
+ const onClose = jest.fn();
+ const onRowClick = jest.fn();
+ const testTitle = 'testTitle';
+ const testInstanceId = 'testInstanceId';
+ const testMutator = {
+ holdings: {
+ GET: jest.fn(() => (new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(
+ [{
+ id: '1',
+ }, {
+ id: '2',
+ }]
+ );
+ });
+ }))),
+ reset: jest.fn(),
+ },
+ items: {
+ GET: jest.fn(() => (new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(
+ [{
+ id: '1',
+ status: {
+ name: itemStatuses.IN_PROCESS,
+ },
+ }, {
+ id: '2',
+ status: {
+ name: itemStatuses.AVAILABLE,
+ },
+ }, {
+ id: '3',
+ status: {
+ name: itemStatuses.IN_TRANSIT,
+ },
+ }]
+ );
+ });
+ }))),
+ reset: jest.fn(),
+ },
+ requests: {
+ GET: jest.fn(() => (new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(
+ [{
+ id: '1',
+ itemId: '1',
+ }, {
+ id: '2',
+ itemId: '2',
+ }, {
+ id: '4',
+ itemId: '1',
+ }]
+ );
+ });
+ }))),
+ reset: jest.fn(),
+ },
+ };
+ const defaultTestProps = {
+ open: false,
+ onClose,
+ onRowClick,
+ instanceId: testInstanceId,
+ title: testTitle,
+ mutator: testMutator,
+ };
+
+ afterEach(() => {
+ Modal.mockClear();
+ MultiColumnList.mockClear();
+ Pane.mockClear();
+ Paneset.mockClear();
+ Loading.mockClear();
+ onClose.mockClear();
+ testMutator.holdings.GET.mockClear();
+ testMutator.holdings.reset.mockClear();
+ testMutator.items.GET.mockClear();
+ testMutator.items.reset.mockClear();
+ testMutator.requests.GET.mockClear();
+ testMutator.requests.reset.mockClear();
+ });
+
+ describe('with default props', () => {
+ beforeEach(() => {
+ render();
+ });
+
+ it('should render Modal', () => {
+ expect(Modal).toHaveBeenCalledWith(
+ expect.objectContaining({
+ 'data-test-move-request-modal': true,
+ label: labelIds.selectItem,
+ open: false,
+ onClose,
+ dismissible: true,
+ }), {}
+ );
+ });
+
+ it('should render Paneset', () => {
+ expect(Paneset).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ id: 'itemsDialog',
+ isRoot: true,
+ static: true,
+ }), {}
+ );
+ });
+
+ it('should render Pane', () => {
+ expect(Pane).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ paneTitle: labelIds.instanceItems,
+ paneSub: labelIds.resultCount,
+ defaultWidth: 'fill',
+ }), {}
+ );
+ });
+
+ it('should not render Loading', () => {
+ expect(Loading).not.toHaveBeenCalled();
+ });
+
+ it('should render MultiColumnList', () => {
+ expect(MultiColumnList).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ id: 'instance-items-list',
+ interactive: true,
+ contentData: [],
+ visibleColumns: COLUMN_NAMES,
+ columnMapping: COLUMN_MAP,
+ columnWidths: COLUMN_WIDTHS,
+ formatter,
+ maxHeight: MAX_HEIGHT,
+ isEmptyMessage: labelIds.instanceItemsNotFound,
+ onRowClick,
+ }), {}
+ );
+ });
+ });
+
+ describe('when open prop is true', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should render Modal', () => {
+ expect(Modal).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ 'data-test-move-request-modal': true,
+ label: labelIds.selectItem,
+ open: true,
+ onClose,
+ dismissible: true,
+ }), {}
+ );
+ });
+
+ it('should render Loading when data is being loaded', () => {
+ const loading = screen.queryByTestId(testIds.loading);
+
+ expect(loading).toBeInTheDocument();
+ });
+
+ describe('when data is loaded', () => {
+ beforeEach(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ });
+
+ it('should hide Loading when data is loaded', () => {
+ const loading = screen.queryByTestId(testIds.loading);
+
+ expect(loading).not.toBeInTheDocument();
+ });
+
+ it('should render MultiColumnList when data is loaded', () => {
+ expect(MultiColumnList).toHaveBeenLastCalledWith(
+ {
+ id: 'instance-items-list',
+ interactive: true,
+ contentData: [{
+ id: '2',
+ status: {
+ name: itemStatuses.AVAILABLE,
+ },
+ requestQueue: 1,
+ }, {
+ id: '1',
+ status: {
+ name: itemStatuses.IN_PROCESS,
+ },
+ requestQueue: 2,
+ }, {
+ id: '3',
+ status: {
+ name: itemStatuses.IN_TRANSIT,
+ },
+ requestQueue: 0,
+ }],
+ visibleColumns: COLUMN_NAMES,
+ columnMapping: COLUMN_MAP,
+ columnWidths: COLUMN_WIDTHS,
+ formatter,
+ maxHeight: MAX_HEIGHT,
+ isEmptyMessage: labelIds.instanceItemsNotFound,
+ onRowClick,
+ }, {}
+ );
+ });
+
+ describe('when "items" response contains non-requestable items', () => {
+ const getProcessedItem = (status) => ({
+ id: status,
+ requestQueue: 0,
+ status: {
+ name: status,
+ },
+ });
+ const allItemStatuses = Object.values(itemStatuses);
+ const newMutator = {
+ ...testMutator,
+ items: {
+ GET: jest.fn(() => (new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(
+ allItemStatuses.map(status => ({
+ id: status,
+ status: {
+ name: status,
+ },
+ })),
+ );
+ });
+ }))),
+ reset: jest.fn(),
+ },
+ requests: {
+ GET: jest.fn(() => (new Promise((resolve) => {
+ setTimeout(() => {
+ resolve([]);
+ });
+ }))),
+ reset: jest.fn(),
+ },
+ };
+
+ describe('within "move request" action', () => {
+ beforeEach(async () => {
+ render(
+
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ });
+
+ it('should show only items with requestable statuses', () => {
+ const requestableItems = requestableItemStatuses.map(getProcessedItem);
+
+ expect(MultiColumnList).toHaveBeenLastCalledWith(expect.objectContaining({
+ contentData: expect.arrayContaining(requestableItems),
+ }), {});
+ });
+
+ it('should not show items with non-requestable statuses', () => {
+ const nonRequestableItems = allItemStatuses.map(status => {
+ if (requestableItemStatuses.includes(status)) {
+ return null;
+ }
+
+ return getProcessedItem(status);
+ });
+
+ expect(MultiColumnList).toHaveBeenLastCalledWith(expect.objectContaining({
+ contentData: expect.not.arrayContaining(nonRequestableItems),
+ }), {});
+ });
+ });
+
+ describe('within switching from instance-level to item-level request', () => {
+ beforeEach(async () => {
+ render(
+
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ });
+
+ it('should show items with all possible statuses', () => {
+ const expectedResult = allItemStatuses.map(getProcessedItem);
+
+ expect(MultiColumnList).toHaveBeenLastCalledWith(expect.objectContaining({
+ contentData: expect.arrayContaining(expectedResult),
+ }), {});
+ });
+ });
+ });
+ });
+ });
+
+ [
+ true,
+ false,
+ ].forEach((isLoading) => {
+ describe(`when isLoading is ${isLoading}`, () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ if (isLoading) {
+ it('should render Loading', () => {
+ expect(Loading).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not render MultiColumnList', () => {
+ expect(MultiColumnList).toHaveBeenCalledTimes(0);
+ });
+ } else {
+ it('should not render Loading', () => {
+ expect(Loading).toHaveBeenCalledTimes(0);
+ });
+
+ it('should render MultiColumnList', () => {
+ expect(MultiColumnList).toHaveBeenLastCalledWith(
+ {
+ id: 'instance-items-list',
+ interactive: true,
+ contentData: [],
+ visibleColumns: COLUMN_NAMES,
+ columnMapping: COLUMN_MAP,
+ columnWidths: COLUMN_WIDTHS,
+ formatter,
+ maxHeight: MAX_HEIGHT,
+ isEmptyMessage: labelIds.instanceItemsNotFound,
+ onRowClick,
+ }, {}
+ );
+ });
+ }
+ });
+ });
+
+ describe('formatter', () => {
+ const item = {
+ status: {
+ name: 'Aged to lost',
+ },
+ effectiveLocation: {
+ name: 'effective location name',
+ },
+ materialType: {
+ name: 'material type name',
+ },
+ temporaryLoanType: {
+ name: 'temporary loan type name',
+ },
+ permanentLoanType: {
+ name: 'permanent loan type name',
+ },
+ };
+
+ describe('itemStatus', () => {
+ it('should return formatted message', () => {
+ expect(formatter.itemStatus(item).props.id).toEqual('ui-requests.item.status.agedToLost');
+ });
+ });
+
+ describe('location', () => {
+ it('should return effective location name', () => {
+ expect(formatter.location(item)).toEqual('effective location name');
+ });
+
+ it('should return default value for effective location name', () => {
+ expect(formatter.location({
+ ...item,
+ effectiveLocation: {},
+ })).toEqual('');
+ });
+ });
+
+ describe('materialType', () => {
+ it('should return material type', () => {
+ expect(formatter.materialType(item)).toEqual('material type name');
+ });
+ });
+
+ describe('loanType', () => {
+ describe('with temporaryLoanType', () => {
+ it('should return temporary loan type name', () => {
+ expect(formatter.loanType(item)).toEqual('temporary loan type name');
+ });
+
+ it('should return default value for temporary loan type name', () => {
+ expect(formatter.loanType({
+ ...item,
+ temporaryLoanType: {
+ other: '',
+ },
+ })).toEqual('');
+ });
+ });
+
+ describe('without temporaryLoanType', () => {
+ it('should return permanent loan type name', () => {
+ expect(formatter.loanType({
+ ...item,
+ temporaryLoanType: false,
+ })).toEqual('permanent loan type name');
+ });
+
+ it('should return default value for permanent loan type name', () => {
+ expect(formatter.loanType({
+ ...item,
+ temporaryLoanType: false,
+ permanentLoanType: {
+ other: '',
+ },
+ })).toEqual('');
+ });
+ });
+ });
+ });
+});
diff --git a/src/deprecated/components/MoveRequestManager/MoveRequestManager.js b/src/deprecated/components/MoveRequestManager/MoveRequestManager.js
new file mode 100644
index 00000000..a8bb5205
--- /dev/null
+++ b/src/deprecated/components/MoveRequestManager/MoveRequestManager.js
@@ -0,0 +1,257 @@
+import {
+ get,
+ includes,
+} from 'lodash';
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage } from 'react-intl';
+
+import {
+ stripesConnect,
+ stripesShape,
+} from '@folio/stripes/core';
+import {
+ getHeaderWithCredentials,
+} from '@folio/stripes/util';
+
+import ItemsDialog from '../ItemsDialog/ItemsDialog';
+import ChooseRequestTypeDialog from '../../../ChooseRequestTypeDialog';
+import ErrorModal from '../../../components/ErrorModal';
+import { getRequestTypeOptions } from '../../../utils';
+import { REQUEST_OPERATIONS } from '../../../constants';
+
+class MoveRequestManager extends React.Component {
+ static propTypes = {
+ onCancelMove: PropTypes.func,
+ onMove: PropTypes.func,
+ request: PropTypes.object.isRequired,
+ mutator: PropTypes.shape({
+ move: PropTypes.shape({
+ POST: PropTypes.func.isRequired,
+ }),
+ moveRequest: PropTypes.shape({
+ POST: PropTypes.func.isRequired,
+ }),
+ }).isRequired,
+ stripes: stripesShape.isRequired,
+ };
+
+ static manifest = {
+ moveRequest: {
+ type: 'okapi',
+ POST: {
+ path: 'circulation/requests/!{request.id}/move',
+ },
+ fetch: false,
+ throwErrors: false,
+ },
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ moveRequest: true,
+ moveInProgress: false,
+ requestTypes: [],
+ isRequestTypesLoading: false,
+ };
+
+ this.steps = [
+ {
+ validate: this.shouldChooseRequestTypeDialogBeShown,
+ exec: () => this.setState({
+ chooseRequestType: true,
+ moveRequest: false,
+ }),
+ },
+ ];
+ }
+
+ execSteps = (start) => {
+ for (let i = start; i < this.steps.length; i++) {
+ const step = this.steps[i];
+ if (step.validate()) {
+ return step.exec();
+ }
+ }
+
+ return this.moveRequest();
+ }
+
+ shouldChooseRequestTypeDialogBeShown = () => {
+ const { requestTypes } = this.state;
+ const {
+ request: {
+ requestType,
+ },
+ } = this.props;
+
+ return !includes(requestTypes, requestType);
+ }
+
+ confirmChoosingRequestType = (selectedRequestType) => {
+ this.setState({
+ selectedRequestType,
+ }, () => this.execSteps(1));
+ }
+
+ moveRequest = async () => {
+ const {
+ selectedItem,
+ selectedRequestType,
+ } = this.state;
+ const {
+ mutator: {
+ moveRequest: { POST },
+ },
+ request,
+ } = this.props;
+ const requestType = selectedRequestType || request.requestType;
+ const destinationItemId = selectedItem.id;
+ const data = {
+ destinationItemId,
+ requestType,
+ };
+
+ this.setState({ moveInProgress: true });
+
+ try {
+ const movedRequest = await POST(data);
+ this.props.onMove(movedRequest);
+
+ this.setState({
+ chooseRequestType: false,
+ });
+ } catch (resp) {
+ this.processError(resp);
+ } finally {
+ this.setState({ moveInProgress: false });
+ }
+ }
+
+ processError(resp) {
+ const contentType = resp.headers.get('Content-Type') || '';
+ if (contentType.startsWith('application/json')) {
+ return resp.json().then(error => this.handleError(get(error, 'errors[0].message')));
+ } else {
+ return resp.text().then(error => this.handleError(error));
+ }
+ }
+
+ handleError(errorMessage) {
+ this.setState({
+ chooseRequestType: false,
+ errorMessage,
+ });
+ }
+
+ onItemSelected = (selectedItem) => {
+ const {
+ request,
+ stripes,
+ } = this.props;
+ const httpHeadersOptions = {
+ ...getHeaderWithCredentials({
+ tenant: stripes.okapi.tenant,
+ token: stripes.store.getState().okapi.token,
+ })
+ };
+ const url = `${stripes.okapi.url}/circulation/requests/allowed-service-points?requestId=${request.id}&itemId=${selectedItem.id}&operation=${REQUEST_OPERATIONS.MOVE}`;
+
+ this.setState({
+ isRequestTypesLoading: true,
+ requestTypes: [],
+ });
+
+ fetch(url, httpHeadersOptions)
+ .then(res => {
+ if (res.ok) {
+ return res.json();
+ }
+
+ return Promise.reject(res);
+ })
+ .then((res) => {
+ this.setState({
+ requestTypes: Object.keys(res),
+ selectedItem,
+ }, () => this.execSteps(0));
+ })
+ .catch(() => {
+ this.execSteps(0);
+ })
+ .finally(() => {
+ this.setState({
+ isRequestTypesLoading: false,
+ });
+ });
+ }
+
+ cancelMoveRequest = () => {
+ this.setState({
+ moveRequest: true,
+ chooseRequestType: false,
+ selectedRequestType: '',
+ });
+ }
+
+ closeErrorMessage = () => {
+ const { requestTypes } = this.state;
+ const state = { errorMessage: null };
+
+ if (requestTypes && requestTypes.length > 1) {
+ state.chooseRequestType = true;
+ } else {
+ state.moveRequest = true;
+ }
+
+ this.setState(state);
+ }
+
+ render() {
+ const {
+ onCancelMove,
+ request,
+ } = this.props;
+ const {
+ chooseRequestType,
+ errorMessage,
+ moveRequest,
+ moveInProgress,
+ requestTypes,
+ isRequestTypesLoading,
+ } = this.state;
+ const isLoading = moveInProgress || isRequestTypesLoading;
+
+ return (
+ <>
+ this.onItemSelected(item)}
+ />
+ {chooseRequestType &&
+ }
+ {errorMessage &&
+ }
+ errorMessage={errorMessage}
+ /> }
+ >
+ );
+ }
+}
+
+export default stripesConnect(MoveRequestManager);
diff --git a/src/deprecated/components/MoveRequestManager/MoveRequestManager.test.js b/src/deprecated/components/MoveRequestManager/MoveRequestManager.test.js
new file mode 100644
index 00000000..1a4f0b17
--- /dev/null
+++ b/src/deprecated/components/MoveRequestManager/MoveRequestManager.test.js
@@ -0,0 +1,404 @@
+import {
+ render,
+ screen,
+ fireEvent,
+ cleanup,
+ waitFor,
+} from '@folio/jest-config-stripes/testing-library/react';
+import userEvent from '@folio/jest-config-stripes/testing-library/user-event';
+
+import MoveRequestManager from './MoveRequestManager';
+import ItemsDialog from '../ItemsDialog/ItemsDialog';
+import ChooseRequestTypeDialog from '../../../ChooseRequestTypeDialog';
+import ErrorModal from '../../../components/ErrorModal';
+import {
+ REQUEST_OPERATIONS,
+ requestTypeOptionMap,
+ requestTypesMap,
+} from '../../../constants';
+
+const labelIds = {
+ requestNotAllowed: 'ui-requests.requestNotAllowed',
+};
+const testIds = {
+ rowButton: 'rowButton',
+ confirmButton: 'confirmButton',
+ cancelButton: 'cancelButton',
+ chooseRequestTypeDialog: 'chooseRequestTypeDialog',
+ errorModal: 'errorModal',
+ closeErrorModalButton: 'closeErrorModalButton',
+};
+const movedRequest = {};
+const basicProps = {
+ onCancelMove: jest.fn(),
+ onMove: jest.fn(),
+ request: {
+ requestType: requestTypesMap.PAGE,
+ instanceId: 'instanceId',
+ itemId: 'itemId',
+ instance: {
+ title: 'instanceTitle',
+ },
+ id: 'requestId',
+ },
+ mutator: {
+ move: {
+ POST: jest.fn(),
+ },
+ moveRequest: {
+ POST: jest.fn(async () => movedRequest),
+ },
+ },
+ stripes: {
+ okapi: {
+ url: 'okapiUrl',
+ tenant: 'okapiTenant',
+ },
+ store: {
+ getState: () => ({
+ okapi: {
+ token: 'okapiToken',
+ },
+ }),
+ },
+ },
+};
+const selectedItem = {
+ id: 'selectedItemId',
+ status: {
+ name: 'Checked out',
+ },
+};
+
+jest.mock('../ItemsDialog/ItemsDialog', () => jest.fn(({
+ children,
+ onRowClick,
+}) => {
+ return (
+
+
+ {children}
+
+ );
+}));
+jest.mock('../../../ChooseRequestTypeDialog', () => jest.fn(({
+ onConfirm,
+ onCancel
+}) => (
+
+
+
+
+)));
+jest.mock('../../../components/ErrorModal', () => jest.fn(({
+ label,
+ onClose,
+}) => (
+
+)));
+
+describe('MoveRequestManager', () => {
+ beforeEach(() => {
+ global.fetch = jest.fn(() => Promise.resolve({
+ json: () => ({
+ [requestTypesMap.HOLD]: ['id'],
+ })
+ }));
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ cleanup();
+ });
+
+ describe('Initial rendering', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should trigger "ItemsDialog" with correct props', () => {
+ const expectedProps = {
+ open: true,
+ instanceId: basicProps.request.instanceId,
+ title: basicProps.request.instance.title,
+ isLoading: false,
+ onClose: basicProps.onCancelMove,
+ skippedItemId: basicProps.request.itemId,
+ onRowClick: expect.any(Function),
+ };
+
+ expect(ItemsDialog).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+
+ it('should not trigger "ChooseRequestTypeDialog"', () => {
+ expect(ChooseRequestTypeDialog).not.toHaveBeenCalled();
+ });
+
+ it('should not trigger "ErrorModal"', () => {
+ expect(ErrorModal).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Request type dialog', () => {
+ beforeEach(() => {
+ global.fetch = jest.fn(() => Promise.resolve({
+ ok: true,
+ json: () => ({
+ [requestTypesMap.HOLD]: ['id'],
+ })
+ }));
+
+ render(
+
+ );
+
+ const rowButton = screen.getByTestId(testIds.rowButton);
+
+ fireEvent.click(rowButton);
+ });
+
+ it('should trigger fetch with correct argument', () => {
+ const expectedUrl = `${basicProps.stripes.okapi.url}/circulation/requests/allowed-service-points?requestId=${basicProps.request.id}&itemId=${selectedItem.id}&operation=${REQUEST_OPERATIONS.MOVE}`;
+
+ expect(global.fetch).toHaveBeenCalledWith(expectedUrl, {});
+ });
+
+ it('should trigger "ChooseRequestTypeDialog" with correct props', async () => {
+ const expectedProps = {
+ open: true,
+ 'data-test-choose-request-type-modal': true,
+ onConfirm: expect.any(Function),
+ onCancel: expect.any(Function),
+ isLoading: false,
+ requestTypes: [
+ {
+ id: requestTypeOptionMap[requestTypesMap.HOLD],
+ value: requestTypesMap.HOLD,
+ }
+ ],
+ };
+
+ await waitFor(() => expect(ChooseRequestTypeDialog).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {}));
+ });
+
+ it('should hide request type dialog after clicking on "Cancel" button', async () => {
+ await waitFor(() => {
+ const cancelButton = screen.getByTestId(testIds.cancelButton);
+ const chooseRequestTypeDialog = screen.getByTestId(testIds.chooseRequestTypeDialog);
+
+ fireEvent.click(cancelButton);
+
+ expect(chooseRequestTypeDialog).not.toBeInTheDocument();
+ });
+ });
+
+ it('should trigger "moveRequest.POST" after clicking on "Confirm" button', async () => {
+ await waitFor(() => {
+ const confirmButton = screen.getByTestId(testIds.confirmButton);
+
+ fireEvent.click(confirmButton);
+
+ expect(basicProps.mutator.moveRequest.POST).toHaveBeenCalled();
+ });
+ });
+
+ it('should trigger "onMove" with correct argument after clicking on "Confirm" button', async () => {
+ await waitFor(async () => {
+ const confirmButton = screen.getByTestId(testIds.confirmButton);
+
+ fireEvent.click(confirmButton);
+
+ await waitFor(() => {
+ expect(basicProps.onMove).toHaveBeenCalledWith(movedRequest);
+ });
+ });
+ });
+ });
+
+ describe('Error modal', () => {
+ describe('When response content type is "application/json"', () => {
+ const error = {
+ errors: [
+ {
+ message: 'Error message',
+ }
+ ]
+ };
+ const get = jest.fn(() => 'application/json');
+ const json = jest.fn(() => new Promise(res => res(error)));
+
+ beforeEach(async () => {
+ global.fetch = jest.fn(() => Promise.resolve({
+ ok: true,
+ json: () => ({
+ [requestTypesMap.HOLD]: ['holdId'],
+ [requestTypesMap.RECALL]: ['recallId'],
+ })
+ }));
+
+ const props = {
+ ...basicProps,
+ mutator: {
+ ...basicProps.mutator,
+ moveRequest: {
+ POST: () => {
+ const errorToThrow = new Error('message');
+
+ errorToThrow.headers = {
+ get,
+ };
+ errorToThrow.json = json;
+
+ throw errorToThrow;
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ const rowButton = screen.getByTestId(testIds.rowButton);
+
+ await userEvent.click(rowButton);
+
+ const confirmButton = screen.getByTestId(testIds.confirmButton);
+
+ await userEvent.click(confirmButton);
+ });
+
+ it('should trigger "ErrorModal" with correct props', async () => {
+ const expectedProps = {
+ onClose: expect.any(Function),
+ errorMessage: error.errors[0].message,
+ };
+
+ await waitFor(() => expect(ErrorModal).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {}));
+ });
+
+ it('should render "ErrorModal" label', async () => {
+ const requestNotAllowedLabel = screen.getByText(labelIds.requestNotAllowed);
+
+ await waitFor(() => expect(requestNotAllowedLabel).toBeInTheDocument());
+ });
+
+ it('should not render "ChooseRequestTypeDialog"', async () => {
+ const chooseRequestTypeDialog = screen.queryByTestId(testIds.chooseRequestTypeDialog);
+
+ await waitFor(() => expect(chooseRequestTypeDialog).not.toBeInTheDocument());
+ });
+
+ it('should hide "ErrorModal"', async () => {
+ const errorModal = screen.getByTestId(testIds.errorModal);
+ const closeErrorModalButton = screen.getByTestId(testIds.closeErrorModalButton);
+
+ fireEvent.click(closeErrorModalButton);
+
+ await waitFor(() => expect(errorModal).not.toBeInTheDocument());
+ });
+
+ it('should render "ChooseRequestTypeDialog" after closing error modal', async () => {
+ const closeErrorModalButton = screen.getByTestId(testIds.closeErrorModalButton);
+
+ fireEvent.click(closeErrorModalButton);
+
+ const chooseRequestTypeDialog = screen.queryByTestId(testIds.chooseRequestTypeDialog);
+
+ await waitFor(() => expect(chooseRequestTypeDialog).toBeInTheDocument());
+ });
+ });
+
+ describe('When response content type is not "application/json"', () => {
+ const error = 'Test error';
+ const get = jest.fn(() => '');
+ const text = jest.fn(() => new Promise(res => res(error)));
+
+ beforeEach(async () => {
+ const props = {
+ ...basicProps,
+ mutator: {
+ ...basicProps.mutator,
+ moveRequest: {
+ POST: () => {
+ const errorToThrow = new Error('message');
+
+ errorToThrow.text = text;
+ errorToThrow.headers = {
+ get,
+ };
+
+ throw errorToThrow;
+ },
+ },
+ },
+ };
+
+ global.fetch = jest.fn(() => Promise.resolve({
+ ok: true,
+ json: () => ({
+ [requestTypesMap.HOLD]: ['holdId'],
+ [requestTypesMap.RECALL]: ['recallId'],
+ })
+ }));
+
+ render(
+
+ );
+
+ const rowButton = screen.getByTestId(testIds.rowButton);
+
+ await waitFor(() => {
+ fireEvent.click(rowButton);
+
+ const confirmButton = screen.getByTestId(testIds.confirmButton);
+
+ fireEvent.click(confirmButton);
+ });
+ });
+
+ it('should trigger "ErrorModal" with correct props', async () => {
+ const expectedProps = {
+ onClose: expect.any(Function),
+ errorMessage: error,
+ };
+
+ await waitFor(() => expect(ErrorModal).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {}));
+ });
+ });
+ });
+});
diff --git a/src/deprecated/components/RequestForm/RequestForm.css b/src/deprecated/components/RequestForm/RequestForm.css
new file mode 100644
index 00000000..9c5c8b4a
--- /dev/null
+++ b/src/deprecated/components/RequestForm/RequestForm.css
@@ -0,0 +1,17 @@
+.requestForm {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
+}
+
+.footerContent {
+ max-width: 50em;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.tlrCheckbox {
+ margin-bottom: 0.5em;
+ user-select: none;
+}
diff --git a/src/deprecated/components/RequestForm/RequestForm.js b/src/deprecated/components/RequestForm/RequestForm.js
new file mode 100644
index 00000000..ba0387d3
--- /dev/null
+++ b/src/deprecated/components/RequestForm/RequestForm.js
@@ -0,0 +1,1412 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ Field,
+} from 'react-final-form';
+import {
+ FormattedMessage,
+} from 'react-intl';
+import { parse } from 'query-string';
+
+import {
+ sortBy,
+ find,
+ get,
+ isEqual,
+ isEmpty,
+ keyBy,
+ defer,
+ pick,
+ isBoolean,
+ isNil,
+} from 'lodash';
+
+import {
+ Accordion,
+ AccordionSet,
+ Button,
+ Col,
+ Pane,
+ PaneFooter,
+ PaneHeaderIconButton,
+ PaneMenu,
+ Paneset,
+ Row,
+ Checkbox,
+ AccordionStatus,
+} from '@folio/stripes/components';
+import stripesFinalForm from '@folio/stripes/final-form';
+
+import RequestFormShortcutsWrapper from '../../../components/RequestFormShortcutsWrapper';
+import CancelRequestDialog from '../../../CancelRequestDialog';
+import PatronBlockModal from '../../../PatronBlockModal';
+import {
+ ErrorModal,
+ RequestInformation,
+ RequesterInformation,
+ FulfilmentPreference,
+} from '../../../components';
+import ItemInformation from '../ItemInformation/ItemInformation';
+import InstanceInformation from '../InstanceInformation/InstanceInformation';
+import ItemsDialog from '../ItemsDialog/ItemsDialog';
+import {
+ iconTypes,
+ fulfillmentTypeMap,
+ createModes,
+ REQUEST_LEVEL_TYPES,
+ RESOURCE_KEYS,
+ REQUEST_FORM_FIELD_NAMES,
+ DEFAULT_REQUEST_TYPE_VALUE,
+ requestTypeOptionMap,
+ REQUEST_LAYERS,
+ REQUEST_OPERATIONS,
+} from '../../../constants';
+import { RESOURCE_TYPES } from '../../constants';
+import {
+ handleKeyCommand,
+ toUserAddress,
+ getPatronGroup,
+ isDelivery,
+ parseErrorMessage,
+ getFulfillmentTypeOptions,
+ getDefaultRequestPreferences,
+ getFulfillmentPreference,
+ isDeliverySelected,
+ getSelectedAddressTypeId,
+ getProxy,
+ isSubmittingButtonDisabled,
+ isFormEditing,
+ resetFieldState,
+ getRequester,
+} from '../../../utils';
+import { getTlrSettings } from '../../utils';
+
+import css from './RequestForm.css';
+
+export const ID_TYPE_MAP = {
+ ITEM_ID: 'itemId',
+ INSTANCE_ID: 'instanceId',
+};
+export const getResourceTypeId = (isTitleLevelRequest) => (isTitleLevelRequest ? ID_TYPE_MAP.INSTANCE_ID : ID_TYPE_MAP.ITEM_ID);
+export const isTLR = (createTitleLevelRequest, requestLevel) => (createTitleLevelRequest || requestLevel === REQUEST_LEVEL_TYPES.TITLE);
+export const getRequestInformation = (values, selectedInstance, selectedItem, request) => {
+ const isTitleLevelRequest = isTLR(values.createTitleLevelRequest, request?.requestLevel);
+ const selectedResource = isTitleLevelRequest ? selectedInstance : selectedItem;
+
+ return {
+ isTitleLevelRequest,
+ selectedResource,
+ };
+};
+
+class RequestForm extends React.Component {
+ static propTypes = {
+ stripes: PropTypes.shape({
+ connect: PropTypes.func.isRequired,
+ store: PropTypes.shape({
+ getState: PropTypes.func.isRequired,
+ }).isRequired,
+ }).isRequired,
+ errorMessage: PropTypes.string,
+ handleSubmit: PropTypes.func.isRequired,
+ findResource: PropTypes.func.isRequired,
+ request: PropTypes.object,
+ metadataDisplay: PropTypes.func,
+ initialValues: PropTypes.object,
+ location: PropTypes.shape({
+ pathname: PropTypes.string.isRequired,
+ search: PropTypes.string,
+ }).isRequired,
+ onCancel: PropTypes.func.isRequired,
+ onCancelRequest: PropTypes.func,
+ pristine: PropTypes.bool,
+ resources: PropTypes.shape({
+ query: PropTypes.object,
+ }),
+ submitting: PropTypes.bool,
+ toggleModal: PropTypes.func,
+ optionLists: PropTypes.shape({
+ addressTypes: PropTypes.arrayOf(PropTypes.object),
+ fulfillmentTypes: PropTypes.arrayOf(PropTypes.object),
+ servicePoints: PropTypes.arrayOf(PropTypes.object),
+ }),
+ patronGroups: PropTypes.arrayOf(PropTypes.object),
+ parentResources: PropTypes.object,
+ history: PropTypes.shape({
+ push: PropTypes.func,
+ }),
+ intl: PropTypes.object,
+ onChangePatron: PropTypes.func,
+ query: PropTypes.object,
+ selectedItem: PropTypes.object,
+ selectedInstance: PropTypes.object,
+ selectedUser: PropTypes.object,
+ values: PropTypes.object.isRequired,
+ form: PropTypes.object.isRequired,
+ blocked: PropTypes.bool.isRequired,
+ instanceId: PropTypes.string.isRequired,
+ isPatronBlocksOverridden: PropTypes.bool.isRequired,
+ onSubmit: PropTypes.func,
+ parentMutator: PropTypes.shape({
+ proxy: PropTypes.shape({
+ reset: PropTypes.func.isRequired,
+ GET: PropTypes.func.isRequired,
+ }).isRequired,
+ }).isRequired,
+ isTlrEnabledOnEditPage: PropTypes.bool,
+ onGetAutomatedPatronBlocks: PropTypes.func.isRequired,
+ onGetPatronManualBlocks: PropTypes.func.isRequired,
+ onSetSelectedItem: PropTypes.func.isRequired,
+ onSetSelectedUser: PropTypes.func.isRequired,
+ onSetSelectedInstance: PropTypes.func.isRequired,
+ onSetBlocked: PropTypes.func.isRequired,
+ onSetIsPatronBlocksOverridden: PropTypes.func.isRequired,
+ onSetInstanceId: PropTypes.func.isRequired,
+ };
+
+ static defaultProps = {
+ request: null,
+ metadataDisplay: () => { },
+ optionLists: {},
+ pristine: true,
+ submitting: false,
+ isTlrEnabledOnEditPage: false,
+ };
+
+ constructor(props) {
+ super(props);
+
+ const {
+ request,
+ initialValues,
+ } = props;
+ const {
+ loan,
+ } = (request || {});
+
+ const { titleLevelRequestsFeatureEnabled } = this.getTlrSettings();
+
+ this.state = {
+ proxy: null,
+ selectedLoan: loan,
+ ...getDefaultRequestPreferences(request, initialValues),
+ isAwaitingForProxySelection: false,
+ titleLevelRequestsFeatureEnabled,
+ isItemOrInstanceLoading: false,
+ isItemsDialogOpen: false,
+ isItemIdRequest: this.isItemIdProvided(),
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ isRequestTypeLoading: false,
+ isRequestTypesForDuplicate: false,
+ isRequestTypesForEditing: false,
+ };
+
+ this.connectedCancelRequestDialog = props.stripes.connect(CancelRequestDialog);
+ this.onChangeAddress = this.onChangeAddress.bind(this);
+ this.onSelectProxy = this.onSelectProxy.bind(this);
+ this.onClose = this.onClose.bind(this);
+ this.accordionStatusRef = React.createRef();
+ }
+
+ componentDidMount() {
+ const {
+ query: {
+ userId,
+ },
+ } = this.props;
+
+ if (this.props.query.userBarcode) {
+ this.findUser(RESOURCE_KEYS.barcode, this.props.query.userBarcode);
+ } else if (userId) {
+ this.findUser(RESOURCE_KEYS.id, userId);
+ }
+
+ if (this.props.query.itemBarcode) {
+ this.findItem(RESOURCE_KEYS.barcode, this.props.query.itemBarcode);
+ }
+
+ if (this.props.query.itemId) {
+ this.findItem(RESOURCE_KEYS.id, this.props.query.itemId);
+ }
+
+ if (this.props.query.instanceId && !this.props.query.itemBarcode && !this.props.query.itemId) {
+ this.findInstance(this.props.query.instanceId);
+ }
+
+ if (isFormEditing(this.props.request)) {
+ this.findRequestPreferences(this.props.initialValues.requesterId);
+ }
+
+ this.setTlrCheckboxInitialState();
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ isRequestTypesForDuplicate,
+ isRequestTypesForEditing,
+ } = this.state;
+ const {
+ initialValues,
+ request,
+ values,
+ parentResources,
+ query,
+ selectedItem,
+ selectedUser,
+ selectedInstance,
+ onGetAutomatedPatronBlocks,
+ onGetPatronManualBlocks,
+ onSetBlocked,
+ onSetSelectedItem,
+ onSetSelectedUser,
+ } = this.props;
+
+ const {
+ initialValues: prevInitialValues,
+ request: prevRequest,
+ parentResources: prevParentResources,
+ query: prevQuery,
+ } = prevProps;
+
+ const prevBlocks = onGetPatronManualBlocks(prevParentResources);
+ const blocks = onGetPatronManualBlocks(parentResources);
+ const prevAutomatedPatronBlocks = onGetAutomatedPatronBlocks(prevParentResources);
+ const automatedPatronBlocks = onGetAutomatedPatronBlocks(parentResources);
+ const { item } = initialValues;
+
+ if (
+ (initialValues &&
+ initialValues.fulfillmentPreference &&
+ prevInitialValues &&
+ !prevInitialValues.fulfillmentPreference) ||
+ !isEqual(request, prevRequest)
+ ) {
+ onSetSelectedItem(request.item);
+ onSetSelectedUser(request.requester);
+ // eslint-disable-next-line react/no-did-update-set-state
+ this.setState({
+ selectedAddressTypeId: initialValues.deliveryAddressTypeId,
+ deliverySelected: isDelivery(initialValues),
+ selectedLoan: request.loan,
+ });
+ }
+
+ // When in duplicate mode there are cases when selectedItem from state
+ // is missing or not set. In this case just set it to initial item.
+ if (query?.mode === createModes.DUPLICATE &&
+ item && !selectedItem) {
+ onSetSelectedItem(item);
+ this.triggerItemBarcodeValidation();
+ }
+
+ if (query?.mode === createModes.DUPLICATE &&
+ (selectedItem?.id || selectedInstance?.id) &&
+ selectedUser?.id &&
+ !isRequestTypesForDuplicate
+ ) {
+ this.setState({
+ isRequestTypesForDuplicate: true,
+ });
+ this.getAvailableRequestTypes(selectedUser);
+ }
+
+ if (query?.layer === REQUEST_LAYERS.EDIT &&
+ !isRequestTypesForEditing
+ ) {
+ this.setState({
+ isRequestTypesForEditing: true,
+ });
+
+ const isTitleLevelRequest = isTLR(values.createTitleLevelRequest, request.requestLevel);
+ const resourceTypeId = getResourceTypeId(isTitleLevelRequest);
+ const resourceId = isTitleLevelRequest ? request.instanceId : request.itemId;
+
+ this.findRequestTypes(resourceId, request.requester.id || request.requesterId, resourceTypeId);
+ }
+
+ if (prevQuery.userBarcode !== query.userBarcode) {
+ this.findUser(RESOURCE_KEYS.barcode, query.userBarcode);
+ }
+
+ if (prevQuery.userId !== query.userId) {
+ this.findUser(RESOURCE_KEYS.id, query.userId);
+ }
+
+ if (prevQuery.itemBarcode !== query.itemBarcode) {
+ this.findItem(RESOURCE_KEYS.barcode, query.itemBarcode);
+ }
+
+ if (prevQuery.itemId !== query.itemId) {
+ this.findItem(RESOURCE_KEYS.id, query.itemId);
+ }
+
+ if (prevQuery.instanceId !== query.instanceId) {
+ this.findInstance(query.instanceId);
+ this.setTlrCheckboxInitialState();
+ }
+
+ if (!isEqual(blocks, prevBlocks) && blocks.length > 0) {
+ const user = selectedUser || {};
+ if (user.id === blocks[0].userId) {
+ onSetBlocked(true);
+ }
+ }
+
+ if (!isEqual(prevAutomatedPatronBlocks, automatedPatronBlocks) && !isEmpty(automatedPatronBlocks)) {
+ if (selectedUser.id) {
+ onSetBlocked(true);
+ }
+ }
+
+ if (prevParentResources?.configs?.records[0]?.value !== parentResources?.configs?.records[0]?.value) {
+ const {
+ titleLevelRequestsFeatureEnabled,
+ } = this.getTlrSettings();
+
+ // eslint-disable-next-line react/no-did-update-set-state
+ this.setState(
+ {
+ titleLevelRequestsFeatureEnabled,
+ },
+ this.setTlrCheckboxInitialState(),
+ );
+ }
+ }
+
+ isItemIdProvided = () => {
+ const {
+ query,
+ location,
+ } = this.props;
+ const itemId = query?.itemId || parse(location.search)?.itemId;
+
+ return Boolean(itemId);
+ }
+
+ getTlrSettings() {
+ return getTlrSettings(this.props.parentResources?.configs?.records[0]?.value);
+ }
+
+ setTlrCheckboxInitialState() {
+ const {
+ form,
+ } = this.props;
+
+ if (this.state.titleLevelRequestsFeatureEnabled === false) {
+ form.change(REQUEST_FORM_FIELD_NAMES.CREATE_TLR, false);
+ return;
+ }
+
+ if (this.props.query.itemId || this.props.query.itemBarcode) {
+ form.change(REQUEST_FORM_FIELD_NAMES.CREATE_TLR, false);
+ } else if (this.props.query.instanceId) {
+ form.change(REQUEST_FORM_FIELD_NAMES.CREATE_TLR, true);
+ }
+ }
+
+ onClose() {
+ this.props.toggleModal();
+ }
+
+ changeDeliveryAddress = (deliverySelected, selectedAddressTypeId) => {
+ this.setState({
+ deliverySelected,
+ selectedAddressTypeId,
+ }, () => {
+ this.updateRequestPreferencesFields();
+ });
+ }
+
+ onChangeAddress(e) {
+ const { form } = this.props;
+ const selectedAddressTypeId = e.target.value;
+
+ form.change(REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, selectedAddressTypeId);
+ this.setState({
+ selectedAddressTypeId,
+ });
+ }
+
+ getAvailableRequestTypes = (user) => {
+ const {
+ selectedItem,
+ selectedInstance,
+ request,
+ values,
+ } = this.props;
+ const {
+ selectedResource,
+ isTitleLevelRequest,
+ } = getRequestInformation(values, selectedInstance, selectedItem, request);
+
+ if (selectedResource?.id && user?.id) {
+ const resourceTypeId = getResourceTypeId(isTitleLevelRequest);
+
+ this.findRequestTypes(selectedResource.id, user.id, resourceTypeId);
+ }
+ }
+
+ // Executed when a user is selected from the proxy dialog,
+ // regardless of whether the selection is "self" or an actual proxy
+ onSelectProxy(proxy) {
+ const {
+ form,
+ selectedUser,
+ } = this.props;
+
+ if (selectedUser.id === proxy.id) {
+ this.setState({
+ proxy: selectedUser,
+ });
+ } else {
+ this.setState({
+ proxy,
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ });
+ form.change(REQUEST_FORM_FIELD_NAMES.REQUESTER_ID, proxy.id);
+ form.change(REQUEST_FORM_FIELD_NAMES.PROXY_USER_ID, selectedUser.id);
+ this.findRequestPreferences(proxy.id);
+ this.getAvailableRequestTypes(proxy);
+ }
+
+ this.setState({ isAwaitingForProxySelection: false });
+ }
+
+ async hasProxies(user) {
+ if (!user) {
+ this.setState({ isAwaitingForProxySelection: false });
+
+ return null;
+ }
+
+ const { parentMutator: mutator } = this.props;
+ const query = `query=(proxyUserId==${user.id})`;
+
+ mutator.proxy.reset();
+
+ const userProxies = await mutator.proxy.GET({ params: { query } });
+
+ if (userProxies.length) {
+ this.setState({ isAwaitingForProxySelection: true });
+ } else {
+ this.setState({ isAwaitingForProxySelection: false });
+ }
+
+ return user;
+ }
+
+ shouldSetBlocked = (blocks, selectedUser) => {
+ return blocks.length && blocks[0].userId === selectedUser.id;
+ }
+
+ findUser = (fieldName, value, isValidation = false) => {
+ const {
+ form,
+ findResource,
+ parentResources,
+ onChangePatron,
+ onGetPatronManualBlocks,
+ onGetAutomatedPatronBlocks,
+ onSetIsPatronBlocksOverridden,
+ onSetSelectedUser,
+ onSetBlocked,
+ } = this.props;
+
+ this.setState({
+ isUserLoading: true,
+ });
+
+ if (isValidation) {
+ return findResource(RESOURCE_TYPES.USER, value, fieldName)
+ .then((result) => {
+ return result.totalRecords;
+ })
+ .finally(() => {
+ this.setState({ isUserLoading: false });
+ });
+ } else {
+ this.setState({
+ proxy: null,
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ });
+ form.change(REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID, undefined);
+ form.change(REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, undefined);
+ form.change(REQUEST_FORM_FIELD_NAMES.PROXY_USER_ID, undefined);
+
+ return findResource(RESOURCE_TYPES.USER, value, fieldName)
+ .then((result) => {
+ this.setState({ isAwaitingForProxySelection: true });
+
+ if (result.totalRecords === 1) {
+ const blocks = onGetPatronManualBlocks(parentResources);
+ const automatedPatronBlocks = onGetAutomatedPatronBlocks(parentResources);
+ const isAutomatedPatronBlocksRequestInPendingState = parentResources.automatedPatronBlocks.isPending;
+ const selectedUser = result.users[0];
+ onChangePatron(selectedUser);
+ form.change(REQUEST_FORM_FIELD_NAMES.REQUESTER_ID, selectedUser.id);
+ form.change(REQUEST_FORM_FIELD_NAMES.REQUESTER, selectedUser);
+ onSetSelectedUser(selectedUser);
+
+ if (fieldName === RESOURCE_KEYS.id) {
+ this.triggerUserBarcodeValidation();
+ }
+
+ this.findRequestPreferences(selectedUser.id);
+
+ if (this.shouldSetBlocked(blocks, selectedUser) || (!isEmpty(automatedPatronBlocks) && !isAutomatedPatronBlocksRequestInPendingState)) {
+ onSetBlocked(true);
+ onSetIsPatronBlocksOverridden(false);
+ }
+
+ return selectedUser;
+ }
+
+ return null;
+ })
+ .then(user => {
+ this.getAvailableRequestTypes(user);
+
+ return user;
+ })
+ .then(user => this.hasProxies(user))
+ .finally(() => {
+ this.setState({ isUserLoading: false });
+ });
+ }
+ }
+
+ async findRequestPreferences(userId) {
+ const {
+ findResource,
+ form,
+ request,
+ initialValues,
+ } = this.props;
+
+ try {
+ const { requestPreferences } = await findResource('requestPreferences', userId, 'userId');
+ const preferences = requestPreferences[0];
+
+ const defaultPreferences = getDefaultRequestPreferences(request, initialValues);
+ const requestPreference = {
+ ...defaultPreferences,
+ ...pick(preferences, ['defaultDeliveryAddressTypeId', 'defaultServicePointId']),
+ requestPreferencesLoaded: true,
+ };
+
+ // when in edit mode (editing existing request) and defaultServicePointId is present (it was
+ // set during creation) just keep it instead of choosing the preferred one.
+ // https://issues.folio.org/browse/UIREQ-544
+ if (isFormEditing(request) && defaultPreferences.defaultServicePointId) {
+ requestPreference.defaultServicePointId = defaultPreferences.defaultServicePointId;
+ }
+
+ const deliveryIsPredefined = get(preferences, 'delivery');
+
+ if (isBoolean(deliveryIsPredefined)) {
+ requestPreference.hasDelivery = deliveryIsPredefined;
+ }
+
+ const fulfillmentPreference = getFulfillmentPreference(preferences, initialValues);
+ const deliverySelected = isDeliverySelected(fulfillmentPreference);
+
+ const selectedAddress = requestPreference.selectedAddressTypeId || requestPreference.defaultDeliveryAddressTypeId;
+
+ const selectedAddressTypeId = getSelectedAddressTypeId(deliverySelected, selectedAddress);
+
+ this.setState({
+ ...requestPreference,
+ deliverySelected,
+ selectedAddressTypeId,
+ }, () => {
+ form.change(REQUEST_FORM_FIELD_NAMES.FULFILLMENT_PREFERENCE, fulfillmentPreference);
+
+ this.updateRequestPreferencesFields();
+ });
+ } catch (e) {
+ this.setState({
+ ...getDefaultRequestPreferences(request, initialValues),
+ deliverySelected: false,
+ }, () => {
+ form.change(REQUEST_FORM_FIELD_NAMES.FULFILLMENT_PREFERENCE, fulfillmentTypeMap.HOLD_SHELF);
+ });
+ }
+ }
+
+ updateRequestPreferencesFields = () => {
+ const {
+ defaultDeliveryAddressTypeId,
+ defaultServicePointId,
+ deliverySelected,
+ selectedAddressTypeId,
+ } = this.state;
+ const {
+ initialValues: {
+ requesterId,
+ },
+ form,
+ selectedUser,
+ } = this.props;
+
+ if (deliverySelected) {
+ const deliveryAddressTypeId = selectedAddressTypeId || defaultDeliveryAddressTypeId;
+
+ form.change(REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, deliveryAddressTypeId);
+ form.change(REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID, '');
+ } else {
+ // Only change pickupServicePointId to defaultServicePointId
+ // if selected user has changed (by choosing a different user manually)
+ // or if the request form is not in a DUPLICATE mode.
+ // In DUPLICATE mode the pickupServicePointId from a duplicated request record will be used instead.
+ if (requesterId !== selectedUser?.id || this.props?.query?.mode !== createModes.DUPLICATE) {
+ form.change(REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID, defaultServicePointId);
+ }
+ form.change(REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, '');
+ }
+ }
+
+ findRequestTypes = (resourceId, requesterId, resourceType) => {
+ const {
+ findResource,
+ form,
+ request,
+ } = this.props;
+ const isEditForm = isFormEditing(request);
+ let requestParams;
+
+ if (isEditForm) {
+ requestParams = {
+ operation: REQUEST_OPERATIONS.REPLACE,
+ requestId: request.id,
+ };
+ } else {
+ requestParams = {
+ operation: REQUEST_OPERATIONS.CREATE,
+ [resourceType]: resourceId,
+ requesterId,
+ };
+ form.change(REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE, DEFAULT_REQUEST_TYPE_VALUE);
+ }
+
+ this.setState({
+ isRequestTypeLoading: true,
+ });
+
+ findResource(RESOURCE_TYPES.REQUEST_TYPES, requestParams)
+ .then(requestTypes => {
+ if (!isEmpty(requestTypes)) {
+ this.setState({
+ requestTypes,
+ isRequestTypesReceived: true,
+ }, this.triggerRequestTypeValidation);
+ } else {
+ this.setState({
+ isRequestTypesReceived: true,
+ }, this.triggerRequestTypeValidation);
+ }
+ })
+ .finally(() => {
+ this.setState({
+ isRequestTypeLoading: false,
+ });
+ });
+ }
+
+ findItemRelatedResources(item) {
+ const {
+ findResource,
+ onSetInstanceId,
+ } = this.props;
+ if (!item) return null;
+
+ return Promise.all(
+ [
+ findResource('loan', item.id),
+ findResource('requestsForItem', item.id),
+ findResource(RESOURCE_TYPES.HOLDING, item.holdingsRecordId),
+ ],
+ ).then((results) => {
+ const selectedLoan = results[0]?.loans?.[0];
+ const itemRequestCount = results[1]?.requests?.length;
+ const holdingsRecord = results[2]?.holdingsRecords?.[0];
+
+ onSetInstanceId(holdingsRecord?.instanceId);
+ this.setState({
+ itemRequestCount,
+ selectedLoan,
+ });
+
+ return item;
+ });
+ }
+
+ setItemIdRequest = (key, isBarcodeRequired) => {
+ const { isItemIdRequest } = this.state;
+
+ if (key === RESOURCE_KEYS.id && !isBarcodeRequired) {
+ this.setState({
+ isItemIdRequest: true,
+ });
+ } else if (key === RESOURCE_KEYS.barcode && isItemIdRequest) {
+ this.setState({
+ isItemIdRequest: false,
+ });
+ }
+ };
+
+ findItem = (key, value, isValidation = false, isBarcodeRequired = false) => {
+ const {
+ findResource,
+ form,
+ onSetSelectedItem,
+ selectedUser,
+ } = this.props;
+ const { proxy } = this.state;
+
+ this.setState({
+ isItemOrInstanceLoading: true,
+ });
+
+ if (isValidation) {
+ return findResource(RESOURCE_TYPES.ITEM, value, key)
+ .then((result) => {
+ return result.totalRecords;
+ })
+ .finally(() => {
+ this.setState({ isItemOrInstanceLoading: false });
+ });
+ } else {
+ this.setState({
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ });
+
+ return findResource(RESOURCE_TYPES.ITEM, value, key)
+ .then((result) => {
+ this.setItemIdRequest(key, isBarcodeRequired);
+
+ if (!result || result.totalRecords === 0) {
+ this.setState({
+ isItemOrInstanceLoading: false,
+ });
+
+ return null;
+ }
+
+ const item = result.items[0];
+
+ form.change(REQUEST_FORM_FIELD_NAMES.ITEM_ID, item.id);
+ form.change(REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, item.barcode);
+ resetFieldState(form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE);
+
+ // Setting state here is redundant with what follows, but it lets us
+ // display the matched item as quickly as possible, without waiting for
+ // the slow loan and request lookups
+ onSetSelectedItem(item);
+ this.setState({
+ isItemOrInstanceLoading: false,
+ });
+
+ return item;
+ })
+ .then(item => {
+ if (item && selectedUser?.id) {
+ const requester = getRequester(proxy, selectedUser);
+ this.findRequestTypes(item.id, requester.id, ID_TYPE_MAP.ITEM_ID);
+ }
+
+ return item;
+ })
+ .then(item => this.findItemRelatedResources(item));
+ }
+ }
+
+ findInstanceRelatedResources(instance) {
+ if (!instance) {
+ return null;
+ }
+
+ const { findResource } = this.props;
+
+ return findResource('requestsForInstance', instance.id)
+ .then((result) => {
+ const instanceRequestCount = result.requests.filter(r => r.requestLevel === REQUEST_LEVEL_TYPES.TITLE).length || 0;
+
+ this.setState({ instanceRequestCount });
+
+ return instance;
+ });
+ }
+
+ findInstance = async (instanceId, holdingsRecordId, isValidation = false) => {
+ const {
+ findResource,
+ form,
+ onSetSelectedInstance,
+ selectedUser,
+ } = this.props;
+ const { proxy } = this.state;
+
+ this.setState({
+ isItemOrInstanceLoading: true,
+ });
+
+ const resultInstanceId = isNil(instanceId)
+ ? await findResource(RESOURCE_TYPES.HOLDING, holdingsRecordId).then((holding) => holding.holdingsRecords[0].instanceId)
+ : instanceId;
+
+ if (isValidation) {
+ return findResource(RESOURCE_TYPES.INSTANCE, resultInstanceId)
+ .then((result) => {
+ return result.totalRecords;
+ })
+ .finally(() => {
+ this.setState({ isItemOrInstanceLoading: false });
+ });
+ } else {
+ this.setState({
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ });
+
+ return findResource(RESOURCE_TYPES.INSTANCE, resultInstanceId)
+ .then((result) => {
+ if (!result || result.totalRecords === 0) {
+ this.setState({
+ isItemOrInstanceLoading: false,
+ });
+
+ return null;
+ }
+
+ const instance = result.instances[0];
+
+ form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, instance.id);
+ form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instance.hrid);
+ resetFieldState(form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE);
+
+ onSetSelectedInstance(instance);
+ this.setState({
+ isItemOrInstanceLoading: false,
+ });
+
+ return instance;
+ })
+ .then(instance => {
+ if (instance && selectedUser?.id) {
+ const requester = getRequester(proxy, selectedUser);
+ this.findRequestTypes(instance.id, requester.id, ID_TYPE_MAP.INSTANCE_ID);
+ }
+
+ return instance;
+ })
+ .then(instance => {
+ this.findInstanceRelatedResources(instance);
+
+ return instance;
+ });
+ }
+ }
+
+ triggerItemBarcodeValidation = () => {
+ const {
+ form,
+ values,
+ } = this.props;
+
+ form.change('keyOfItemBarcodeField', values.keyOfItemBarcodeField ? 0 : 1);
+ };
+
+ triggerUserBarcodeValidation = () => {
+ const {
+ form,
+ values,
+ } = this.props;
+
+ form.change('keyOfUserBarcodeField', values.keyOfUserBarcodeField ? 0 : 1);
+ };
+
+ triggerInstanceIdValidation = () => {
+ const {
+ form,
+ values,
+ } = this.props;
+
+ form.change('keyOfInstanceIdField', values.keyOfInstanceIdField ? 0 : 1);
+ };
+
+ triggerRequestTypeValidation = () => {
+ const {
+ form,
+ values,
+ } = this.props;
+
+ form.change('keyOfRequestTypeField', values.keyOfRequestTypeField ? 0 : 1);
+ };
+
+ onCancelRequest = (cancellationInfo) => {
+ this.setState({ isCancellingRequest: false });
+ this.props.onCancelRequest(cancellationInfo);
+ }
+
+ onCloseBlockedModal = () => {
+ const {
+ onSetBlocked,
+ } = this.props;
+
+ onSetBlocked(false);
+ }
+
+ onViewUserPath(selectedUser, patronGroup) {
+ // reinitialize form (mark it as pristine)
+ this.props.form.reset();
+ // wait for the form to be reinitialized
+ defer(() => {
+ this.setState({ isCancellingRequest: false });
+ const viewUserPath = `/users/view/${(selectedUser || {}).id}?filters=pg.${patronGroup.group}`;
+ this.props.history.push(viewUserPath);
+ });
+ }
+
+ renderAddRequestFirstMenu = () => (
+
+
+ {title => (
+
+ )}
+
+
+ );
+
+ overridePatronBlocks = () => {
+ const {
+ onSetIsPatronBlocksOverridden,
+ } = this.props;
+
+ onSetIsPatronBlocksOverridden(true);
+ };
+
+ handleTlrCheckboxChange = (event) => {
+ const isCreateTlr = event.target.checked;
+ const {
+ form,
+ selectedItem,
+ selectedInstance,
+ onSetSelectedItem,
+ onSetSelectedInstance,
+ } = this.props;
+
+ form.change(REQUEST_FORM_FIELD_NAMES.CREATE_TLR, isCreateTlr);
+ form.change(REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, null);
+ form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, null);
+ form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, null);
+
+ if (isCreateTlr) {
+ onSetSelectedItem(undefined);
+ this.setState({
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ });
+
+ if (selectedItem) {
+ this.findInstance(null, selectedItem.holdingsRecordId);
+ }
+ } else if (selectedInstance) {
+ form.change(REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE, DEFAULT_REQUEST_TYPE_VALUE);
+ resetFieldState(form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE);
+ this.setState({
+ isItemsDialogOpen: true,
+ });
+ } else {
+ onSetSelectedInstance(undefined);
+ this.setState({
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ });
+ }
+ };
+
+ handleItemsDialogClose = () => {
+ const {
+ onSetSelectedInstance,
+ } = this.props;
+
+ onSetSelectedInstance(undefined);
+ this.setState({
+ isItemsDialogOpen: false,
+ requestTypes: {},
+ isRequestTypesReceived: false,
+ isItemIdRequest: false,
+ }, this.triggerItemBarcodeValidation);
+ }
+
+ handleInstanceItemClick = (event, item) => {
+ const {
+ onSetSelectedInstance,
+ } = this.props;
+ let isBarcodeRequired = false;
+
+ onSetSelectedInstance(undefined);
+ this.setState({
+ isItemsDialogOpen: false,
+ requestTypes: {},
+ });
+
+ if (item?.barcode) {
+ isBarcodeRequired = true;
+ this.setState({
+ isItemIdRequest: false,
+ });
+ }
+
+ this.findItem(RESOURCE_KEYS.id, item.id, false, isBarcodeRequired);
+ }
+
+ handleCloseProxy = () => {
+ const {
+ onSetSelectedUser,
+ } = this.props;
+
+ onSetSelectedUser(null);
+ this.setState({
+ proxy: null,
+ });
+ };
+
+ render() {
+ const {
+ handleSubmit,
+ request,
+ form,
+ optionLists: {
+ addressTypes,
+ },
+ patronGroups,
+ parentResources,
+ submitting,
+ intl: {
+ formatMessage,
+ },
+ errorMessage,
+ selectedItem,
+ selectedUser,
+ selectedInstance,
+ isPatronBlocksOverridden,
+ instanceId,
+ blocked,
+ values,
+ onCancel,
+ onGetAutomatedPatronBlocks,
+ onGetPatronManualBlocks,
+ isTlrEnabledOnEditPage,
+ optionLists,
+ pristine,
+ onSetSelectedItem,
+ onSetSelectedInstance,
+ metadataDisplay,
+ } = this.props;
+
+ const {
+ selectedLoan,
+ itemRequestCount,
+ instanceRequestCount,
+ selectedAddressTypeId,
+ deliverySelected,
+ isCancellingRequest,
+ isUserLoading,
+ isItemOrInstanceLoading,
+ isAwaitingForProxySelection,
+ isItemsDialogOpen,
+ proxy,
+ requestTypes,
+ hasDelivery,
+ defaultDeliveryAddressTypeId,
+ isItemIdRequest,
+ isRequestTypesReceived,
+ isRequestTypeLoading,
+ } = this.state;
+ const {
+ createTitleLevelRequest,
+ } = values;
+ const patronBlocks = onGetPatronManualBlocks(parentResources);
+ const automatedPatronBlocks = onGetAutomatedPatronBlocks(parentResources);
+ const isEditForm = isFormEditing(request);
+ const selectedProxy = getProxy(request, proxy);
+ const requester = getRequester(selectedProxy, selectedUser);
+ let deliveryLocations;
+ let deliveryLocationsDetail = [];
+ let addressDetail;
+ if (requester?.personal?.addresses) {
+ deliveryLocations = requester.personal.addresses.map((a) => {
+ const typeName = find(addressTypes, { id: a.addressTypeId }).addressType;
+ return { label: typeName, value: a.addressTypeId };
+ });
+ deliveryLocations = sortBy(deliveryLocations, ['label']);
+ deliveryLocationsDetail = keyBy(requester.personal.addresses, a => a.addressTypeId);
+ }
+
+ if (selectedAddressTypeId) {
+ addressDetail = toUserAddress(deliveryLocationsDetail[selectedAddressTypeId]);
+ }
+
+ const patronGroup = getPatronGroup(requester, patronGroups);
+ const fulfillmentTypeOptions = getFulfillmentTypeOptions(hasDelivery, optionLists?.fulfillmentTypes || []);
+ const isSubmittingDisabled = isSubmittingButtonDisabled(pristine, submitting);
+ const isTitleLevelRequest = createTitleLevelRequest || request?.requestLevel === REQUEST_LEVEL_TYPES.TITLE;
+ const getPatronBlockModalOpenStatus = () => {
+ if (isAwaitingForProxySelection) {
+ return false;
+ }
+
+ const isBlockedAndOverriden = blocked && !isPatronBlocksOverridden;
+
+ return proxy?.id
+ ? isBlockedAndOverriden && (proxy.id === selectedUser?.id)
+ : isBlockedAndOverriden;
+ };
+
+ const handleCancelAndClose = () => {
+ const keepEditBtn = document.getElementById('clickable-cancel-editing-confirmation-confirm');
+ if (isItemsDialogOpen) handleKeyCommand(this.handleItemsDialogClose);
+ else if (errorMessage) this.onClose();
+ else if (keepEditBtn) keepEditBtn.click();
+ else onCancel();
+ };
+ const isFulfilmentPreferenceVisible = (values.requestType || isEditForm) && !isRequestTypeLoading && isRequestTypesReceived;
+ const requestTypeOptions = Object.keys(requestTypes).map(requestType => {
+ return {
+ id: requestTypeOptionMap[requestType],
+ value: requestType,
+ };
+ });
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+export default stripesFinalForm({
+ navigationCheck: true,
+ subscription: {
+ values: true,
+ },
+})(RequestForm);
diff --git a/src/deprecated/components/RequestForm/RequestForm.test.js b/src/deprecated/components/RequestForm/RequestForm.test.js
new file mode 100644
index 00000000..25cad731
--- /dev/null
+++ b/src/deprecated/components/RequestForm/RequestForm.test.js
@@ -0,0 +1,1868 @@
+import { Router } from 'react-router-dom';
+import { createMemoryHistory } from 'history';
+
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+} from '@folio/jest-config-stripes/testing-library/react';
+import {
+ CommandList,
+ defaultKeyboardShortcuts,
+} from '@folio/stripes/components';
+
+import RequestForm, {
+ getRequestInformation,
+ getResourceTypeId,
+ ID_TYPE_MAP,
+} from './RequestForm';
+import FulfilmentPreference from '../../../components/FulfilmentPreference';
+import RequesterInformation from '../../../components/RequesterInformation';
+
+import {
+ REQUEST_LEVEL_TYPES,
+ createModes,
+ REQUEST_LAYERS,
+ REQUEST_FORM_FIELD_NAMES,
+ DEFAULT_REQUEST_TYPE_VALUE,
+ RESOURCE_KEYS,
+ REQUEST_OPERATIONS,
+} from '../../../constants';
+import { RESOURCE_TYPES } from '../../constants';
+import {
+ getDefaultRequestPreferences,
+ isFormEditing,
+ getFulfillmentPreference,
+ resetFieldState,
+ getRequester,
+} from '../../../utils';
+import { getTlrSettings } from '../../utils';
+
+const testIds = {
+ tlrCheckbox: 'tlrCheckbox',
+ instanceInfoSection: 'instanceInfoSection',
+ fulfilmentPreferenceField: 'fulfilmentPreferenceField',
+ addressFiled: 'addressFiled',
+ itemField: 'itemField',
+ requesterField: 'requesterField',
+ instanceField: 'instanceField',
+ selectProxyButton: 'selectProxyButton',
+ closeProxyButton: 'closeProxyButton',
+ overridePatronButton: 'overridePatronButton',
+ closePatronModalButton: 'closePatronModalButton',
+ itemDialogCloseButton: 'itemDialogCloseButton',
+ itemDialogRow: 'itemDialogRow',
+};
+const fieldValue = 'value';
+const idResourceType = 'id';
+const instanceId = 'instanceId';
+const item = {
+ id: 'itemId',
+ barcode: 'itemBarcode',
+};
+
+jest.mock('../../../utils', () => ({
+ ...jest.requireActual('../../../utils'),
+ getRequestLevelValue: jest.fn(),
+ resetFieldState: jest.fn(),
+ getDefaultRequestPreferences: jest.fn(),
+ isFormEditing: jest.fn(),
+ getFulfillmentPreference: jest.fn(),
+ getRequester: jest.fn((proxy, selectedUser) => selectedUser),
+}));
+jest.mock('../../utils', () => ({
+ getTlrSettings: jest.fn(() => ({
+ titleLevelRequestsFeatureEnabled: true,
+ })),
+}));
+jest.mock('../../../components/FulfilmentPreference', () => jest.fn(({
+ changeDeliveryAddress,
+ onChangeAddress,
+}) => {
+ const handleFulfilmentPreferences = () => {
+ changeDeliveryAddress(true);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}));
+jest.mock('../../../components/RequesterInformation', () => jest.fn(({
+ findUser,
+}) => {
+ const handleChange = () => {
+ findUser(idResourceType, fieldValue, true);
+ };
+
+ return (
+
+ );
+}));
+jest.mock('../../../components/RequestInformation', () => jest.fn(() => ));
+jest.mock('../ItemInformation/ItemInformation', () => jest.fn(({
+ findItem,
+ triggerValidation,
+}) => {
+ const handleChange = () => {
+ triggerValidation();
+ findItem(idResourceType, fieldValue, true);
+ };
+
+ return (
+
+ );
+}));
+jest.mock('../InstanceInformation/InstanceInformation', () => jest.fn(({
+ findInstance,
+ triggerValidation,
+}) => {
+ const handleChange = () => {
+ triggerValidation();
+ findInstance(instanceId, null, true);
+ };
+
+ return (
+
+ );
+}));
+jest.mock('@folio/stripes/final-form', () => () => jest.fn((component) => component));
+jest.mock('../../../PatronBlockModal', () => jest.fn(({
+ onOverride,
+ onClose,
+}) => (
+ <>
+
+
+ >
+)));
+jest.mock('../../../CancelRequestDialog', () => jest.fn(() => ));
+jest.mock('../../../components/TitleInformation', () => jest.fn(() => ));
+jest.mock('../../../ItemDetail', () => jest.fn(() => ));
+jest.mock('../ItemsDialog/ItemsDialog', () => jest.fn(({
+ onClose,
+ onRowClick,
+}) => {
+ const handleRowClick = () => {
+ onRowClick({}, item);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}));
+jest.mock('../../../PositionLink', () => jest.fn(() => ));
+
+describe('RequestForm', () => {
+ const labelIds = {
+ tlrCheckbox: 'ui-requests.requests.createTitleLevelRequest',
+ instanceInformation: 'ui-requests.instance.information',
+ enterButton:'ui-requests.enter',
+ requestInfoAccordion: 'ui-requests.requestMeta.information',
+ requesterInfoAccordion: 'ui-requests.requester.information',
+ };
+ const basicProps = {
+ onGetPatronManualBlocks: jest.fn(),
+ onGetAutomatedPatronBlocks: jest.fn(),
+ onSetSelectedInstance: jest.fn(),
+ onSetSelectedItem: jest.fn(),
+ onSetSelectedUser: jest.fn(),
+ onSetInstanceId: jest.fn(),
+ onSetIsPatronBlocksOverridden: jest.fn(),
+ onSetBlocked: jest.fn(),
+ onShowErrorModal: jest.fn(),
+ onHideErrorModal: jest.fn(),
+ onChangePatron: jest.fn(),
+ form: {
+ change: jest.fn(),
+ },
+ handleSubmit: jest.fn(),
+ asyncValidate: jest.fn(),
+ initialValues: {},
+ location: {
+ pathname: 'pathname',
+ },
+ onCancel: jest.fn(),
+ parentMutator: {
+ proxy: {
+ reset: jest.fn(),
+ GET: jest.fn(),
+ },
+ },
+ onSubmit: jest.fn(),
+ parentResources: {
+ patronBlocks: {
+ records: [],
+ },
+ automatedPatronBlocks: {
+ isPending: false,
+ },
+ },
+ intl: {
+ formatMessage: ({ id }) => id,
+ },
+ stripes: {
+ connect: jest.fn((component) => component),
+ store: {
+ getState: jest.fn(),
+ },
+ },
+ values: {
+ deliveryAddressTypeId: '',
+ pickupServicePointId: '',
+ createTitleLevelRequest: '',
+ },
+ findResource: jest.fn(() => new Promise((resolve) => resolve())),
+ request: {},
+ query: {},
+ selectedItem: {},
+ selectedUser: {},
+ selectedInstance: {},
+ isPatronBlocksOverridden: false,
+ isErrorModalOpen: false,
+ blocked: false,
+ optionLists: {
+ addressTypes: [
+ {
+ id: 'addressTypeId',
+ addressType: 'Home',
+ }
+ ],
+ },
+ };
+ const defaultPreferences = {
+ defaultServicePointId: 'defaultServicePointId',
+ defaultDeliveryAddressTypeId: 'defaultDeliveryAddressTypeId',
+ };
+ const fulfillmentPreference = {};
+ const renderComponent = (passedProps = basicProps) => {
+ const history = createMemoryHistory();
+ const { rerender } = render(
+
+
+
+
+
+ );
+
+ return rerender;
+ };
+
+ getDefaultRequestPreferences.mockReturnValue(defaultPreferences);
+ getFulfillmentPreference.mockReturnValue(fulfillmentPreference);
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when `TLR` in enabled', () => {
+ describe('when `isEdit` is false', () => {
+ beforeEach(() => {
+ isFormEditing.mockReturnValue(false);
+ });
+
+ describe('Initial render', () => {
+ const holding = {
+ holdingsRecords: [
+ {
+ instanceId: 'instanceId',
+ }
+ ],
+ };
+ const selectedItem = {
+ holdingsRecordId: 'holdingsRecordId',
+ };
+ let findResource;
+
+ beforeEach(() => {
+ findResource = jest.fn()
+ .mockResolvedValueOnce(holding)
+ .mockResolvedValueOnce(null);
+
+ const props = {
+ ...basicProps,
+ selectedItem,
+ findResource,
+ };
+
+ renderComponent(props);
+ });
+
+ it('should render `TLR` checkbox section', () => {
+ const tlrCheckbox = screen.getByTestId(testIds.tlrCheckbox);
+
+ expect(tlrCheckbox).toBeVisible();
+ });
+
+ it('should reset instance and item info on TLR checkbox change', () => {
+ const expectedArgs = [
+ ['item.barcode', null],
+ ['instance.hrid', null],
+ ['instanceId', null]
+ ];
+ const tlrCheckbox = screen.getByTestId(testIds.tlrCheckbox);
+
+ fireEvent.click(tlrCheckbox);
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toBeCalledWith(...args);
+ });
+ });
+
+ it('should reset selected item', () => {
+ const tlrCheckbox = screen.getByTestId(testIds.tlrCheckbox);
+
+ fireEvent.click(tlrCheckbox);
+
+ expect(basicProps.onSetSelectedItem).toHaveBeenCalledWith(undefined);
+ });
+
+ it('should get instance id if it is not provided', () => {
+ const expectedArgs = [RESOURCE_TYPES.HOLDING, selectedItem.holdingsRecordId];
+ const tlrCheckbox = screen.getByTestId(testIds.tlrCheckbox);
+
+ fireEvent.click(tlrCheckbox);
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('`TLR` checkbox handle on first render', () => {
+ describe('when only `itemId` is passed in query', () => {
+ const props = {
+ ...basicProps,
+ query: {
+ itemId: 'itemId',
+ }
+ };
+
+ it('should set form `createTitleLevelRequest` value to false', () => {
+ const expectedArgs = ['createTitleLevelRequest', false];
+
+ renderComponent(props);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('when only `itemBarcode` is passed in query', () => {
+ const props = {
+ ...basicProps,
+ query: {
+ itemBarcode: 'itemBarcode',
+ }
+ };
+
+ it('should set form `createTitleLevelRequest` value to false', () => {
+ const expectedArgs = ['createTitleLevelRequest', false];
+
+ renderComponent(props);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('when only `instanceId` is passed in query', () => {
+ const props = {
+ ...basicProps,
+ query: {
+ instanceId: 'instanceId',
+ }
+ };
+
+ it('should set form `createTitleLevelRequest` value to true', () => {
+ const expectedArgs = ['createTitleLevelRequest', true];
+
+ renderComponent(props);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+
+ describe('when `TLR` checkbox is checked', () => {
+ const props = {
+ ...basicProps,
+ values: {
+ ...basicProps.values,
+ createTitleLevelRequest: true,
+ },
+ };
+
+ beforeEach(() => {
+ renderComponent(props);
+ });
+
+ it('should render Accordion with `Instance` information', () => {
+ const instanceInformation = screen.getByText(labelIds.instanceInformation);
+
+ expect(instanceInformation).toBeVisible();
+ });
+ });
+
+ describe('when `TLR` checkbox is unchecked', () => {
+ const props = {
+ ...basicProps,
+ values: {
+ ...basicProps.values,
+ createTitleLevelRequest: false,
+ },
+ };
+
+ it('should not render Accordion with `Instance` information', () => {
+ renderComponent(props);
+
+ const instanceInformation = screen.queryByText(labelIds.instanceInformation);
+
+ expect(instanceInformation).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('when `isEdit` is true', () => {
+ const mockedInstance = {
+ id: 'instanceId',
+ title: 'instanceTitle',
+ contributors: 'instanceContributors',
+ publication: 'instancePublication',
+ editions: 'instanceEditions',
+ identifiers: 'instanceIdentifiers',
+ };
+ const mockedRequest = {
+ instance : mockedInstance,
+ id : 'testId',
+ instanceId : 'instanceId',
+ requestType: 'Hold',
+ status: 'Open - Awaiting delivery',
+ };
+
+ beforeEach(() => {
+ isFormEditing.mockReturnValue(true);
+ });
+
+ it('should not render `TLR` checkbox section', () => {
+ const props = {
+ ...basicProps,
+ request: mockedRequest,
+ };
+
+ renderComponent(props);
+
+ const tlrCheckbox = screen.queryByTestId(testIds.tlrCheckbox);
+
+ expect(tlrCheckbox).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('when `TLR` is disabled', () => {
+ beforeEach(() => {
+ getTlrSettings.mockReturnValue({
+ titleLevelRequestsFeatureEnabled: false,
+ });
+ renderComponent();
+ });
+
+ it('should not display `TLR` checkbox', () => {
+ const tlrCheckbox = screen.queryByTestId(testIds.tlrCheckbox);
+
+ expect(tlrCheckbox).not.toBeInTheDocument();
+ });
+
+ it('should set form `createTitleLevelRequest` value to false', () => {
+ const expectedArgs = ['createTitleLevelRequest', false];
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('when duplicate a request', () => {
+ const initialProps = {
+ ...basicProps,
+ query: {
+ mode: createModes.DUPLICATE,
+ },
+ selectedUser: {
+ id: 'userId',
+ },
+ request: {
+ requestLevel: REQUEST_LEVEL_TYPES.TITLE,
+ },
+ initialValues: {
+ item: {
+ id: 'itemId',
+ },
+ },
+ };
+ const findResource = jest.fn().mockResolvedValue({});
+ const newProps = {
+ ...initialProps,
+ selectedInstance: {
+ id: 'instanceId',
+ },
+ selectedItem: null,
+ findResource,
+ };
+
+ beforeEach(() => {
+ const rerender = renderComponent(initialProps);
+
+ isFormEditing.mockReturnValue(false);
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should trigger "findResource" to find request types', () => {
+ expect(findResource).toHaveBeenCalledWith(RESOURCE_TYPES.REQUEST_TYPES, {
+ [ID_TYPE_MAP.INSTANCE_ID]: newProps.selectedInstance.id,
+ requesterId: newProps.selectedUser.id,
+ operation: REQUEST_OPERATIONS.CREATE,
+ });
+ });
+
+ it('should set selected item', () => {
+ expect(basicProps.onSetSelectedItem).toHaveBeenCalledWith(initialProps.initialValues.item);
+ });
+
+ it('should trigger item barcode field validation', () => {
+ const expectedArgs = ['keyOfItemBarcodeField', expect.any(Number)];
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('Request information', () => {
+ const requesterId = 'requesterId';
+
+ beforeEach(() => {
+ const newProps = {
+ ...basicProps,
+ query: {
+ layer: REQUEST_LAYERS.EDIT,
+ },
+ request: {
+ requestLevel: REQUEST_LEVEL_TYPES.TITLE,
+ requester: {},
+ requesterId,
+ instanceId,
+ },
+ values: {
+ ...basicProps.values,
+ requestType: 'requestType',
+ createTitleLevelRequest: true,
+ },
+ selectedUser: {
+ id: 'userId',
+ personal: {
+ addresses: [
+ {
+ addressTypeId: basicProps.optionLists.addressTypes[0].id,
+ }
+ ],
+ },
+ },
+ };
+ const rerender = renderComponent();
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should render request information accordion', () => {
+ const requestInfoAccordion = screen.getByText(labelIds.requestInfoAccordion);
+
+ expect(requestInfoAccordion).toBeInTheDocument();
+ });
+
+ it('should trigger "FulfilmentPreference" with provided delivery locations', async () => {
+ const expectedProps = {
+ deliveryLocations: [
+ {
+ label: basicProps.optionLists.addressTypes[0].addressType,
+ value: basicProps.optionLists.addressTypes[0].id,
+ }
+ ],
+ };
+
+ await waitFor(() => {
+ expect(FulfilmentPreference).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+ });
+
+ it('should handle changing of address field', async () => {
+ const addressField = await screen.findByTestId(testIds.addressFiled);
+ const event = {
+ target: {
+ value: 'selectedAddressTypeId',
+ },
+ };
+ const expectedArgs = [REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, event.target.value];
+
+ fireEvent.change(addressField, event);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should handle changing of fulfilment preferences field', async () => {
+ const fulfilmentPreferenceField = await screen.findByTestId(testIds.fulfilmentPreferenceField);
+ const event = {
+ target: {
+ value: 'fulfilmentPreferences',
+ },
+ };
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, defaultPreferences.defaultDeliveryAddressTypeId],
+ [REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID, '']
+ ];
+
+ fireEvent.change(fulfilmentPreferenceField, event);
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should trigger "form.change" with correct arguments', () => {
+ const expectedArgs = [REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE, DEFAULT_REQUEST_TYPE_VALUE];
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should trigger "findResource" with correct arguments', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.REQUEST_TYPES,
+ {
+ [ID_TYPE_MAP.INSTANCE_ID]: instanceId,
+ requesterId,
+ operation: REQUEST_OPERATIONS.CREATE,
+ }
+ ];
+
+ expect(basicProps.findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('Requester information', () => {
+ const requestPreferencesResult = {
+ requestPreferences: [
+ {
+ delivery: true,
+ }
+ ],
+ };
+ const manualBlocks = [
+ {
+ userId: 'id',
+ }
+ ];
+ const userResult = {
+ totalRecords: 1,
+ users: [
+ {
+ id: 'userId',
+ }
+ ],
+ };
+
+ beforeEach(() => {
+ isFormEditing.mockReturnValue(false);
+ basicProps.onGetPatronManualBlocks.mockReturnValue(manualBlocks);
+ });
+
+ describe('When userId is presented', () => {
+ const initialUserId = 'userId';
+ const updatedUserId = 'updatedUserId';
+
+ describe('Initial rendering', () => {
+ const automatedBlocks = [{}];
+ const userProxies = [];
+ let findResource;
+
+ beforeEach(() => {
+ basicProps.onGetAutomatedPatronBlocks.mockReturnValue(automatedBlocks);
+ basicProps.parentMutator.proxy.GET.mockResolvedValue(userProxies);
+ findResource = jest.fn()
+ .mockResolvedValueOnce(userResult)
+ .mockResolvedValueOnce(requestPreferencesResult)
+ .mockResolvedValue({});
+
+ const props = {
+ ...basicProps,
+ findResource,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userId: initialUserId,
+ },
+ };
+
+ renderComponent(props);
+ });
+
+ it('should render requester information accordion', () => {
+ const requesterInfoAccordion = screen.getByText(labelIds.requesterInfoAccordion);
+
+ expect(requesterInfoAccordion).toBeInTheDocument();
+ });
+
+ it('should reset user related fields', () => {
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID, undefined],
+ [REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, undefined],
+ [REQUEST_FORM_FIELD_NAMES.PROXY_USER_ID, undefined]
+ ];
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should set user related information', () => {
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.REQUESTER_ID, userResult.users[0].id],
+ [REQUEST_FORM_FIELD_NAMES.REQUESTER, userResult.users[0]]
+ ];
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should get user data using user id', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.USER,
+ initialUserId,
+ RESOURCE_KEYS.id,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should get manual blocks information', () => {
+ expect(basicProps.onGetPatronManualBlocks).toHaveBeenCalledWith(basicProps.parentResources);
+ });
+
+ it('should get automated blocks information', () => {
+ expect(basicProps.onGetAutomatedPatronBlocks).toHaveBeenCalledWith(basicProps.parentResources);
+ });
+
+ it('should change patron information', () => {
+ expect(basicProps.onChangePatron).toHaveBeenCalledWith(userResult.users[0]);
+ });
+
+ it('should set selected user', () => {
+ expect(basicProps.onSetSelectedUser).toHaveBeenCalledWith(userResult.users[0]);
+ });
+
+ it('should trigger validation of user barcode field', () => {
+ const expectedArg = ['keyOfUserBarcodeField', 1];
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArg);
+ });
+
+ it('should get request preferences data', () => {
+ const expectedArgs = [
+ 'requestPreferences',
+ userResult.users[0].id,
+ 'userId',
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should set fulfilment preference information', async () => {
+ const expectedArgs = [
+ REQUEST_FORM_FIELD_NAMES.FULFILLMENT_PREFERENCE,
+ fulfillmentPreference
+ ];
+
+ await waitFor(() => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ it('should set blocked to true', () => {
+ expect(basicProps.onSetBlocked).toHaveBeenCalledWith(true);
+ });
+
+ it('should set isPatronBlocksOverridden to false', () => {
+ expect(basicProps.onSetIsPatronBlocksOverridden).toHaveBeenCalledWith(false);
+ });
+
+ it('should set default service point', async () => {
+ const expectedArgs = [
+ REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID,
+ defaultPreferences.defaultServicePointId
+ ];
+
+ await waitFor(() => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ it('should reset delivery address type id', async () => {
+ const expectedArgs = [
+ REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID,
+ ''
+ ];
+
+ await waitFor(() => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ it('should reset proxy information', () => {
+ expect(basicProps.parentMutator.proxy.reset).toHaveBeenCalled();
+ });
+
+ it('should get proxy information', () => {
+ const expectedArg = {
+ params: {
+ query: `query=(proxyUserId==${userResult.users[0].id})`,
+ },
+ };
+
+ expect(basicProps.parentMutator.proxy.GET).toHaveBeenCalledWith(expectedArg);
+ });
+
+ it('should handle requester barcode field change', () => {
+ const expectedArgs = [RESOURCE_TYPES.USER, fieldValue, RESOURCE_KEYS.id];
+ const requesterField = screen.getByTestId(testIds.requesterField);
+ const event = {
+ target: {
+ value: 'value',
+ },
+ };
+
+ fireEvent.change(requesterField, event);
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('Component updating', () => {
+ const findResource = jest.fn(() => Promise.resolve({}));
+ const props = {
+ ...basicProps,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userId: initialUserId,
+ },
+ findResource,
+ };
+ const newProps = {
+ ...basicProps,
+ values: {},
+ request: {},
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userId: updatedUserId,
+ },
+ findResource,
+ };
+
+ beforeEach(() => {
+ const rerender = renderComponent(props);
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should get user data by user id', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.USER,
+ updatedUserId,
+ RESOURCE_KEYS.id,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('Proxy handling', () => {
+ const selectedUser = {
+ id: 'userId',
+ };
+ let findResource;
+
+ beforeEach(() => {
+ const automatedBlocks = [];
+
+ findResource = jest.fn()
+ .mockResolvedValueOnce(userResult)
+ .mockResolvedValueOnce(requestPreferencesResult);
+ basicProps.onGetAutomatedPatronBlocks.mockReturnValue(automatedBlocks);
+ });
+
+ describe('When user acts as a proxy', () => {
+ const proxy = {
+ id: 'proxyId',
+ };
+
+ beforeEach(() => {
+ const props = {
+ ...basicProps,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userId: initialUserId,
+ },
+ selectedUser,
+ findResource,
+ };
+ const userProxies = [
+ {
+ ...proxy,
+ }
+ ];
+
+ RequesterInformation.mockImplementation(({
+ onSelectProxy,
+ handleCloseProxy,
+ }) => {
+ const handleSelectProxy = () => {
+ onSelectProxy(proxy);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+ });
+ basicProps.parentMutator.proxy.GET.mockResolvedValue(userProxies);
+
+ renderComponent(props);
+ });
+
+ it('should set selected user', () => {
+ const selectProxyButton = screen.getByTestId(testIds.selectProxyButton);
+
+ fireEvent.click(selectProxyButton);
+
+ expect(basicProps.onSetSelectedUser).toHaveBeenCalledWith(selectedUser);
+ });
+
+ it('should change requester related fields', () => {
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.REQUESTER_ID, proxy.id],
+ [REQUEST_FORM_FIELD_NAMES.PROXY_USER_ID, selectedUser.id]
+ ];
+ const selectProxyButton = screen.getByTestId(testIds.selectProxyButton);
+
+ fireEvent.click(selectProxyButton);
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should set selected user to null', () => {
+ const closeProxyButton = screen.getByTestId(testIds.closeProxyButton);
+
+ fireEvent.click(closeProxyButton);
+
+ expect(basicProps.onSetSelectedUser).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('When user acts as himself', () => {
+ const proxy = {
+ id: selectedUser.id,
+ };
+
+ beforeEach(() => {
+ const props = {
+ ...basicProps,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userId: initialUserId,
+ },
+ selectedUser,
+ findResource,
+ };
+ const userProxies = [
+ {
+ ...proxy,
+ }
+ ];
+
+ RequesterInformation.mockImplementation(({
+ onSelectProxy,
+ }) => {
+ const handleSelectProxy = () => {
+ onSelectProxy(proxy);
+ };
+
+ return (
+ <>
+
+ >
+ );
+ });
+ basicProps.parentMutator.proxy.GET.mockResolvedValue(userProxies);
+
+ renderComponent(props);
+ });
+
+ it('should change requester related fields', () => {
+ const expectedArgs = [REQUEST_FORM_FIELD_NAMES.REQUESTER_ID, selectedUser.id];
+ const selectProxyButton = screen.getByTestId(testIds.selectProxyButton);
+
+ fireEvent.click(selectProxyButton);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+ });
+
+ describe('When userBarcode is presented', () => {
+ const initialUserBarcode = 'userBarcode';
+ const updatedUserBarcode = 'updatedUserBarcode';
+
+ describe('Initial rendering', () => {
+ const automatedBlocks = [];
+ const userProxies = [{}];
+ let findResource;
+
+ beforeEach(() => {
+ basicProps.onGetAutomatedPatronBlocks.mockReturnValue(automatedBlocks);
+ basicProps.parentMutator.proxy.GET.mockResolvedValue(userProxies);
+ findResource = jest.fn()
+ .mockResolvedValueOnce(userResult)
+ .mockResolvedValueOnce(requestPreferencesResult);
+
+ const props = {
+ ...basicProps,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userBarcode: initialUserBarcode,
+ },
+ findResource,
+ };
+
+ renderComponent(props);
+ });
+
+ it('should reset user related fields', () => {
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.PICKUP_SERVICE_POINT_ID, undefined],
+ [REQUEST_FORM_FIELD_NAMES.DELIVERY_ADDRESS_TYPE_ID, undefined],
+ [REQUEST_FORM_FIELD_NAMES.PROXY_USER_ID, undefined]
+ ];
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should get user data using barcode', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.USER,
+ initialUserBarcode,
+ RESOURCE_KEYS.barcode,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should not trigger validation of user barcode field', () => {
+ const expectedArg = ['keyOfUserBarcodeField', expect.any(Number)];
+
+ expect(basicProps.form.change).not.toHaveBeenCalledWith(...expectedArg);
+ });
+
+ it('should not set blocked to true', () => {
+ expect(basicProps.onSetBlocked).not.toHaveBeenCalledWith(true);
+ });
+
+ it('should not set isPatronBlocksOverridden to false', () => {
+ expect(basicProps.onSetIsPatronBlocksOverridden).not.toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe('Component updating', () => {
+ const findResource = jest.fn(() => Promise.resolve({}));
+ const props = {
+ ...basicProps,
+ findResource,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userBarcode: initialUserBarcode,
+ },
+ };
+ const newProps = {
+ ...basicProps,
+ findResource,
+ query: {
+ layer: REQUEST_LAYERS.CREATE,
+ userBarcode: updatedUserBarcode,
+ },
+ };
+
+ beforeEach(() => {
+ const rerender = renderComponent(props);
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should get user data by user barcode', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.USER,
+ updatedUserBarcode,
+ RESOURCE_KEYS.barcode,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+ });
+
+ describe('Item information', () => {
+ describe('When item barcode is presented', () => {
+ const initialItemBarcode = 'itemBarcode';
+ const updatedItemBarcode = 'updatedItemBarcode';
+
+ describe('Initial render', () => {
+ const requestPreferencesResult = {};
+
+ beforeEach(() => {
+ isFormEditing.mockReturnValue(true);
+ });
+
+ describe('When item is found', () => {
+ const event = {
+ target: {
+ value: 'barcode',
+ },
+ };
+ const itemResult = {
+ totalRecords: 1,
+ items: [
+ {
+ id: 'itemId',
+ barcode: initialItemBarcode,
+ holdingsRecordId: 'holdingsRecordId',
+ }
+ ],
+ };
+ const requestTypesResult = {
+ 'Page': [
+ {
+ id: 'id',
+ name: 'Circ Desk 1',
+ }
+ ]
+ };
+ const loanResult = {
+ loans: [
+ {
+ id: 'loanId',
+ }
+ ],
+ };
+ const itemRequestsResult = {
+ requests: [],
+ };
+ const holdingsRecordResult = {
+ holdingsRecords: [
+ {
+ instanceId,
+ }
+ ],
+ };
+ let findResource;
+
+ beforeEach(() => {
+ findResource = jest.fn()
+ .mockResolvedValueOnce(itemResult)
+ .mockResolvedValueOnce(requestPreferencesResult)
+ .mockResolvedValueOnce(requestTypesResult)
+ .mockResolvedValueOnce(loanResult)
+ .mockResolvedValueOnce(itemRequestsResult)
+ .mockResolvedValueOnce(holdingsRecordResult)
+ .mockResolvedValue({});
+
+ const props = {
+ ...basicProps,
+ selectedUser: {
+ id: 'selectedUserId',
+ },
+ query: {
+ itemBarcode: initialItemBarcode,
+ },
+ request: {
+ id: 'requestId',
+ },
+ findResource,
+ };
+
+ renderComponent(props);
+ });
+
+ it('should get information about requested item', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.ITEM,
+ initialItemBarcode,
+ RESOURCE_KEYS.barcode
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should set item information', () => {
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.ITEM_ID, itemResult.items[0].id],
+ [REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, itemResult.items[0].barcode]
+ ];
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should reset field state for request type', () => {
+ const expectedArgs = [basicProps.form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE];
+
+ expect(resetFieldState).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should get requester information', () => {
+ expect(getRequester).toHaveBeenCalled();
+ });
+
+ it('should get information about loans', () => {
+ const expectedArgs = [
+ 'loan',
+ itemResult.items[0].id
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should get information about open item requests', () => {
+ const expectedArgs = [
+ 'requestsForItem',
+ itemResult.items[0].id
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should get information about holdings', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.HOLDING,
+ itemResult.items[0].holdingsRecordId
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should set instance id', () => {
+ expect(basicProps.onSetInstanceId).toHaveBeenCalledWith(holdingsRecordResult.holdingsRecords[0].instanceId);
+ });
+
+ it('should handle item barcode field change', () => {
+ const expectedArgs = [RESOURCE_TYPES.ITEM, fieldValue, RESOURCE_KEYS.id];
+ const itemField = screen.getByTestId(testIds.itemField);
+
+ fireEvent.change(itemField, event);
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should trigger item barcode field validation', () => {
+ const expectedArgs = ['keyOfItemBarcodeField', expect.any(Number)];
+ const itemField = screen.getByTestId(testIds.itemField);
+
+ fireEvent.change(itemField, event);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('When item is not found', () => {
+ const itemResult = {
+ totalRecords: 0,
+ items: [],
+ };
+ let findResource;
+
+ beforeEach(() => {
+ findResource = jest.fn()
+ .mockResolvedValueOnce(itemResult)
+ .mockResolvedValueOnce(requestPreferencesResult);
+
+ const props = {
+ ...basicProps,
+ selectedUser: {
+ id: 'selectedUserId',
+ },
+ query: {
+ itemBarcode: initialItemBarcode,
+ },
+ request: {
+ id: 'requestId',
+ },
+ findResource,
+ };
+
+ renderComponent(props);
+ });
+
+ it('should get information about requested item', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.ITEM,
+ initialItemBarcode,
+ RESOURCE_KEYS.barcode
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should not reset field state for request type', () => {
+ const expectedArgs = [basicProps.form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE];
+
+ expect(resetFieldState).not.toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should not get request types information', () => {
+ const expectedArgs = [RESOURCE_TYPES.REQUEST_TYPES, expect.any(Object)];
+
+ expect(findResource).not.toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+
+ describe('Component updating', () => {
+ const findResource = jest.fn(() => Promise.resolve());
+ const props = {
+ ...basicProps,
+ query: {
+ itemBarcode: initialItemBarcode,
+ },
+ findResource,
+ };
+ const newProps = {
+ ...basicProps,
+ query: {
+ itemBarcode: updatedItemBarcode,
+ },
+ findResource,
+ };
+
+ beforeEach(() => {
+ const rerender = renderComponent(props);
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should get item data by item barcode', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.ITEM,
+ updatedItemBarcode,
+ RESOURCE_KEYS.barcode,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+
+ describe('When item id is presented', () => {
+ const initialItemId = 'itemId';
+ const updatedItemId = 'updatedItemId';
+ const itemResult = {
+ totalRecords: 0,
+ items: [],
+ };
+
+ describe('Initial render', () => {
+ const findResource = jest.fn().mockResolvedValueOnce(itemResult);
+ const props = {
+ ...basicProps,
+ query: {
+ itemId: initialItemId,
+ },
+ findResource,
+ };
+
+ beforeEach(() => {
+ renderComponent(props);
+ });
+
+ it('should get information about requested item by item id', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.ITEM,
+ initialItemId,
+ RESOURCE_KEYS.id
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('Component updating', () => {
+ const findResource = jest.fn().mockResolvedValue(itemResult);
+ const props = {
+ ...basicProps,
+ query: {
+ itemId: initialItemId,
+ },
+ findResource,
+ };
+ const newProps = {
+ ...basicProps,
+ query: {
+ itemId: updatedItemId,
+ },
+ findResource,
+ };
+
+ beforeEach(() => {
+ const rerender = renderComponent(props);
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should get information about requested item after component updating', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.ITEM,
+ updatedItemId,
+ RESOURCE_KEYS.id
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+ });
+
+ describe('Instance information', () => {
+ const initialInstanceId = 'instanceId';
+ const updatedInstanceId = 'updatedInstanceId';
+
+ describe('Initial render', () => {
+ describe('When instance is found', () => {
+ const event = {
+ target: {
+ value: 'value',
+ },
+ };
+ const instanceResult = {
+ totalRecords: 1,
+ instances: [
+ {
+ id: initialInstanceId,
+ hrid: 'hrid',
+ }
+ ],
+ };
+ const requestTypesResult = {
+ 'Page': [
+ {
+ id: 'id',
+ name: 'Circ Desk 1',
+ }
+ ]
+ };
+ const instanceRequestsResult = {
+ requests: [
+ {
+ requestLevel: REQUEST_LEVEL_TYPES.ITEM,
+ }
+ ],
+ };
+ let findResource;
+
+ beforeEach(() => {
+ findResource = jest.fn()
+ .mockResolvedValueOnce(instanceResult)
+ .mockResolvedValueOnce(requestTypesResult)
+ .mockResolvedValue(instanceRequestsResult);
+
+ const props = {
+ ...basicProps,
+ selectedUser: {
+ id: 'selectedUserId',
+ },
+ query: {
+ instanceId: initialInstanceId,
+ },
+ request: {
+ requestLevel: REQUEST_LEVEL_TYPES.TITLE,
+ },
+ findResource,
+ };
+
+ renderComponent(props);
+ });
+
+ it('should get information about requested instance', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.INSTANCE,
+ initialInstanceId,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should set instance information', () => {
+ const expectedArgs = [
+ [REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, instanceResult.instances[0].id],
+ [REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instanceResult.instances[0].hrid]
+ ];
+
+ expectedArgs.forEach(args => {
+ expect(basicProps.form.change).toHaveBeenCalledWith(...args);
+ });
+ });
+
+ it('should reset field state for request type', () => {
+ const expectedArgs = [basicProps.form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE];
+
+ expect(resetFieldState).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should get requester information', () => {
+ expect(getRequester).toHaveBeenCalled();
+ });
+
+ it('should get information about open instance requests', () => {
+ const expectedArgs = [
+ 'requestsForInstance',
+ instanceResult.instances[0].id
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should set selected instance', () => {
+ expect(basicProps.onSetSelectedInstance).toHaveBeenCalledWith(instanceResult.instances[0]);
+ });
+
+ it('should handle instance id field change', () => {
+ const expectedArgs = [RESOURCE_TYPES.INSTANCE, instanceId];
+ const instanceField = screen.getByTestId(testIds.instanceField);
+
+ fireEvent.change(instanceField, event);
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should trigger instance id field validation', () => {
+ const expectedArgs = ['keyOfInstanceIdField', expect.any(Number)];
+ const instanceField = screen.getByTestId(testIds.instanceField);
+
+ fireEvent.change(instanceField, event);
+
+ expect(basicProps.form.change).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+
+ describe('When instance is not found', () => {
+ const instanceResult = {
+ totalRecords: 0,
+ instances: [],
+ };
+ let findResource;
+
+ beforeEach(() => {
+ findResource = jest.fn().mockResolvedValueOnce(instanceResult);
+
+ const props = {
+ ...basicProps,
+ query: {
+ instanceId: initialInstanceId,
+ },
+ findResource,
+ };
+
+ renderComponent(props);
+ });
+
+ it('should get information about requested instance', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.INSTANCE,
+ initialInstanceId,
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should not get request types information', () => {
+ const expectedArgs = [RESOURCE_TYPES.REQUEST_TYPES, expect.any(Object)];
+
+ expect(findResource).not.toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should not reset field state for request type', () => {
+ const expectedArgs = [basicProps.form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE];
+
+ expect(resetFieldState).not.toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+
+ describe('Component updating', () => {
+ const findResource = jest.fn(() => Promise.resolve());
+ const props = {
+ ...basicProps,
+ findResource,
+ query: {
+ instanceId: initialInstanceId,
+ },
+ };
+ const newProps = {
+ ...basicProps,
+ findResource,
+ query: {
+ instanceId: updatedInstanceId,
+ },
+ };
+
+ beforeEach(() => {
+ const rerender = renderComponent(props);
+
+ rerender(
+
+
+
+
+
+ );
+ });
+
+ it('should get information about requested instance after component updating', () => {
+ const expectedArgs = [
+ RESOURCE_TYPES.INSTANCE,
+ updatedInstanceId
+ ];
+
+ expect(findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+ });
+ });
+
+ describe('Patron block modal', () => {
+ beforeEach(() => {
+ renderComponent();
+ });
+
+ it('should set isPatronBlocksOverridden to true', () => {
+ const overridePatronButton = screen.getByTestId(testIds.overridePatronButton);
+
+ fireEvent.click(overridePatronButton);
+
+ expect(basicProps.onSetIsPatronBlocksOverridden).toHaveBeenCalledWith(true);
+ });
+
+ it('should set blocked to false', () => {
+ const closePatronModalButton = screen.getByTestId(testIds.closePatronModalButton);
+
+ fireEvent.click(closePatronModalButton);
+
+ expect(basicProps.onSetBlocked).toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe('Items dialog', () => {
+ beforeEach(() => {
+ renderComponent();
+ });
+
+ it('should get information about selected item', () => {
+ const expectedArgs = [RESOURCE_TYPES.ITEM, item.id, RESOURCE_KEYS.id];
+ const itemDialogRow = screen.getByTestId(testIds.itemDialogRow);
+
+ fireEvent.click(itemDialogRow);
+
+ expect(basicProps.findResource).toHaveBeenCalledWith(...expectedArgs);
+ });
+
+ it('should reset selected instance', () => {
+ const itemDialogCloseButton = screen.getByTestId(testIds.itemDialogCloseButton);
+
+ fireEvent.click(itemDialogCloseButton);
+
+ expect(basicProps.onSetSelectedInstance).toHaveBeenCalledWith(undefined);
+ });
+ });
+
+ describe('getResourceTypeId', () => {
+ it('should return instance id type', () => {
+ expect(getResourceTypeId(true)).toBe(ID_TYPE_MAP.INSTANCE_ID);
+ });
+
+ it('should return item id type', () => {
+ expect(getResourceTypeId(false)).toBe(ID_TYPE_MAP.ITEM_ID);
+ });
+ });
+
+ describe('getRequestInformation', () => {
+ describe('when title level request', () => {
+ const selectedInstance = {
+ id: 'instanceId',
+ };
+ const args = [
+ {},
+ selectedInstance,
+ {},
+ {
+ requestLevel: REQUEST_LEVEL_TYPES.TITLE,
+ },
+ ];
+
+ it('should return correct data', () => {
+ const expectedResult = {
+ isTitleLevelRequest: true,
+ selectedResource: selectedInstance,
+ };
+
+ expect(getRequestInformation(...args)).toEqual(expectedResult);
+ });
+ });
+
+ describe('when item level request', () => {
+ const selectedItem = {
+ id: 'itemId',
+ };
+ const args = [
+ {},
+ {},
+ selectedItem,
+ {
+ requestLevel: REQUEST_LEVEL_TYPES.ITEM,
+ },
+ ];
+
+ it('should return correct data', () => {
+ const expectedResult = {
+ isTitleLevelRequest: false,
+ selectedResource: selectedItem,
+ };
+
+ expect(getRequestInformation(...args)).toEqual(expectedResult);
+ });
+ });
+ });
+});
diff --git a/src/deprecated/components/RequestFormContainer/RequestFormContainer.js b/src/deprecated/components/RequestFormContainer/RequestFormContainer.js
new file mode 100644
index 00000000..4b60e9d7
--- /dev/null
+++ b/src/deprecated/components/RequestFormContainer/RequestFormContainer.js
@@ -0,0 +1,202 @@
+import {
+ useState,
+} from 'react';
+import {
+ useIntl,
+} from 'react-intl';
+import {
+ cloneDeep,
+ isEmpty,
+ isString,
+ unset,
+} from 'lodash';
+import moment from 'moment-timezone';
+import PropTypes from 'prop-types';
+
+import RequestForm from '../RequestForm/RequestForm';
+import {
+ getRequestLevelValue,
+} from '../../../utils';
+import {
+ fulfillmentTypeMap,
+ REQUEST_LEVEL_TYPES,
+} from '../../../constants';
+import { RESOURCE_TYPES } from '../../constants';
+
+const RequestFormContainer = ({
+ parentResources,
+ request,
+ onSubmit,
+ ...rest
+}) => {
+ const {
+ requester,
+ requesterId,
+ item,
+ } = request || {};
+ const intl = useIntl();
+ const [selectedItem, setSelectedItem] = useState(item);
+ const [selectedUser, setSelectedUser] = useState({ ...requester, id: requesterId });
+ const [selectedInstance, setSelectedInstance] = useState(request?.instance);
+ const [isPatronBlocksOverridden, setIsPatronBlocksOverridden] = useState(false);
+ const [instanceId, setInstanceId] = useState('');
+ const [blocked, setBlocked] = useState(false);
+
+ const setItem = (optedItem) => {
+ setSelectedItem(optedItem);
+ };
+
+ const setUser = (user) => {
+ setSelectedUser(user);
+ };
+
+ const setInstance = (instance) => {
+ setSelectedInstance(instance);
+ };
+
+ const setIsBlocked = (value) => {
+ setBlocked(value);
+ };
+
+ const setStateIsPatronBlocksOverridden = (value) => {
+ setIsPatronBlocksOverridden(value);
+ };
+
+ const setStateInstanceId = (id) => {
+ setInstanceId(id);
+ };
+
+ const getPatronManualBlocks = (resources) => {
+ return (resources?.patronBlocks?.records || [])
+ .filter(b => b.requests === true)
+ .filter(p => moment(moment(p.expirationDate).format()).isSameOrAfter(moment().format()));
+ };
+
+ const getAutomatedPatronBlocks = (resources) => {
+ const automatedPatronBlocks = resources?.automatedPatronBlocks?.records || [];
+
+ return automatedPatronBlocks.reduce((blocks, block) => {
+ if (block.blockRequests) {
+ blocks.push(block.message);
+ }
+
+ return blocks;
+ }, []);
+ };
+
+ const hasBlocking = () => {
+ const [block = {}] = getPatronManualBlocks(parentResources);
+ const automatedPatronBlocks = getAutomatedPatronBlocks(parentResources);
+ const isBlocked = (
+ (block?.userId === selectedUser.id || !isEmpty(automatedPatronBlocks)) &&
+ !isPatronBlocksOverridden
+ );
+
+ setIsBlocked(isBlocked);
+
+ return isBlocked;
+ };
+
+ const handleSubmit = (data) => {
+ const {
+ timeZone,
+ } = intl;
+
+ const requestData = cloneDeep(data);
+
+ const {
+ requestExpirationDate,
+ holdShelfExpirationDate,
+ holdShelfExpirationTime,
+ fulfillmentPreference,
+ deliveryAddressTypeId,
+ pickupServicePointId,
+ } = requestData;
+
+ if (hasBlocking()) return undefined;
+
+ if (!requestExpirationDate) {
+ unset(requestData, 'requestExpirationDate');
+ }
+ if (holdShelfExpirationDate) {
+ // Recombine the values from datepicker and timepicker into a single date/time
+ const date = moment.tz(holdShelfExpirationDate, timeZone).format('YYYY-MM-DD');
+ const time = holdShelfExpirationTime.replace('Z', '');
+ const combinedDateTime = moment.tz(`${date} ${time}`, timeZone);
+ requestData.holdShelfExpirationDate = combinedDateTime.utc().format();
+ } else {
+ unset(requestData, 'holdShelfExpirationDate');
+ }
+ if (fulfillmentPreference === fulfillmentTypeMap.HOLD_SHELF && isString(deliveryAddressTypeId)) {
+ unset(requestData, 'deliveryAddressTypeId');
+ }
+ if (fulfillmentPreference === fulfillmentTypeMap.DELIVERY && isString(pickupServicePointId)) {
+ unset(requestData, 'pickupServicePointId');
+ }
+
+ if (isPatronBlocksOverridden) {
+ requestData.requestProcessingParameters = {
+ overrideBlocks: {
+ patronBlock: {},
+ },
+ };
+ }
+
+ requestData.instanceId = request?.instanceId || instanceId || selectedInstance?.id;
+ requestData.requestLevel = request?.requestLevel || getRequestLevelValue(requestData.createTitleLevelRequest);
+
+ if (requestData.requestLevel === REQUEST_LEVEL_TYPES.ITEM) {
+ requestData.holdingsRecordId = request?.holdingsRecordId || selectedItem?.holdingsRecordId;
+ }
+
+ if (requestData.requestLevel === REQUEST_LEVEL_TYPES.TITLE) {
+ unset(requestData, 'itemId');
+ unset(requestData, 'holdingsRecordId');
+ unset(requestData, RESOURCE_TYPES.ITEM);
+ }
+
+ unset(requestData, 'itemRequestCount');
+ unset(requestData, 'titleRequestCount');
+ unset(requestData, 'createTitleLevelRequest');
+ unset(requestData, 'numberOfReorderableRequests');
+ unset(requestData, RESOURCE_TYPES.INSTANCE);
+ unset(requestData, 'keyOfItemBarcodeField');
+ unset(requestData, 'keyOfUserBarcodeField');
+ unset(requestData, 'keyOfInstanceIdField');
+ unset(requestData, 'keyOfRequestTypeField');
+
+ return onSubmit(requestData);
+ };
+
+ return (
+
+ );
+};
+
+RequestFormContainer.propTypes = {
+ request: PropTypes.object,
+ parentResources: PropTypes.object,
+ onSubmit: PropTypes.func.isRequired,
+};
+
+export default RequestFormContainer;
diff --git a/src/deprecated/components/RequestFormContainer/RequestFormContainer.test.js b/src/deprecated/components/RequestFormContainer/RequestFormContainer.test.js
new file mode 100644
index 00000000..6c858f75
--- /dev/null
+++ b/src/deprecated/components/RequestFormContainer/RequestFormContainer.test.js
@@ -0,0 +1,388 @@
+import {
+ render,
+ screen,
+ fireEvent,
+} from '@folio/jest-config-stripes/testing-library/react';
+
+import RequestFormContainer from './RequestFormContainer';
+import RequestForm from '../RequestForm/RequestForm';
+import {
+ REQUEST_LEVEL_TYPES,
+ fulfillmentTypeMap,
+} from '../../../constants';
+
+jest.mock('../RequestForm/RequestForm', () => jest.fn(() => ));
+
+const defaultProps = {
+ parentResources: {},
+ request: {
+ item: {},
+ instance: {},
+ requester: {},
+ requesterId: 'requesterId',
+ instanceId: 'instanceId',
+ holdingsRecordId: '',
+ },
+ onSubmit: jest.fn(),
+};
+const testIds = {
+ requestForm: 'requestForm',
+};
+
+describe('RequestFormContainer', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Initial render', () => {
+ beforeEach(() => {
+ render(
+
+ );
+ });
+
+ it('should trigger RequestForm with correct props', () => {
+ const expectedProps = {
+ parentResources: defaultProps.parentResources,
+ request: defaultProps.request,
+ blocked: false,
+ selectedItem: defaultProps.request.item,
+ selectedUser: {
+ ...defaultProps.request.requester,
+ id: defaultProps.request.requesterId,
+ },
+ selectedInstance: defaultProps.request.instance,
+ isPatronBlocksOverridden: false,
+ instanceId: '',
+ onGetPatronManualBlocks: expect.any(Function),
+ onGetAutomatedPatronBlocks: expect.any(Function),
+ onSetBlocked: expect.any(Function),
+ onSetSelectedItem: expect.any(Function),
+ onSetSelectedUser: expect.any(Function),
+ onSetSelectedInstance: expect.any(Function),
+ onSetIsPatronBlocksOverridden: expect.any(Function),
+ onSetInstanceId: expect.any(Function),
+ onSubmit: expect.any(Function),
+ };
+
+ expect(RequestForm).toHaveBeenCalledWith(expect.objectContaining(expectedProps), {});
+ });
+ });
+
+ describe('Submit handling', () => {
+ const basicSubmitData = {
+ requestExpirationDate: null,
+ fulfillmentPreference: null,
+ holdShelfExpirationDate: null,
+ pickupServicePointId: 'pickupServicePointId',
+ deliveryAddressTypeId: 'deliveryAddressTypeId',
+ holdingsRecordId: null,
+ itemRequestCount: null,
+ titleRequestCount: null,
+ createTitleLevelRequest: false,
+ numberOfReorderableRequests: null,
+ instance: null,
+ keyOfItemBarcodeField: 0,
+ keyOfUserBarcodeField: 0,
+ keyOfInstanceIdField: 0,
+ keyOfRequestTypeField: 0,
+ };
+
+ describe('When item level request', () => {
+ const requestExpirationDate = new Date().toISOString();
+ const submitData = {
+ ...basicSubmitData,
+ requestLevel: REQUEST_LEVEL_TYPES.ITEM,
+ fulfillmentPreference: fulfillmentTypeMap.HOLD_SHELF,
+ requestExpirationDate,
+ };
+ const props = {
+ ...defaultProps,
+ itemId: 'itemId',
+ item: {
+ id: 'id',
+ },
+ };
+ const selectedItem = {
+ holdingsRecordId: 'holdingsRecordId',
+ };
+ const selectItemLabel = 'Select Item';
+
+ beforeEach(() => {
+ RequestForm.mockImplementation(({
+ onSubmit,
+ onSetSelectedItem,
+ }) => (
+ <>
+