diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3326e45..f7e5b9f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* Designate Organization as donor. Refs UIORGS-383.
* Settings for banking information. Refs UIORGS-391.
+* Implement organization's banking information form. Refs UIORGS-390.
## [5.0.0](https://github.com/folio-org/ui-organizations/tree/v5.0.0) (2023-10-12)
[Full Changelog](https://github.com/folio-org/ui-organizations/compare/v4.0.0...v5.0.0)
diff --git a/package.json b/package.json
index 158442d4..e2fbba66 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"home": "/organizations",
"okapiInterfaces": {
"acquisition-methods": "1.0",
+ "banking-information": "1.0",
"data-export-spring": "1.0",
"configuration": "2.0",
"organizations.organizations": "1.0",
@@ -80,6 +81,8 @@
"data-export.config.item.get",
"orders.acquisition-methods.collection.get",
"orders.acquisition-method.item.get",
+ "organizations.banking-information.collection.get",
+ "organizations.banking-information.item.get",
"organizations.organizations.collection.get",
"organizations.organizations.item.get",
"organizations-storage.addresses.collection.get",
@@ -108,6 +111,7 @@
"data-export.config.item.delete",
"data-export.config.item.post",
"data-export.config.item.put",
+ "organizations.banking-information.item.put",
"organizations.organizations.item.put",
"organizations-storage.addresses.item.put",
"organizations-storage.emails.item.put",
@@ -127,6 +131,7 @@
"displayName": "Organizations: View, edit, create",
"visible": true,
"subPermissions": [
+ "organizations.banking-information.item.post",
"organizations.organizations.item.post",
"organizations-storage.addresses.item.post",
"organizations-storage.emails.item.post",
@@ -140,6 +145,7 @@
"displayName": "Organizations: View, edit, delete",
"visible": true,
"subPermissions": [
+ "organizations.banking-information.item.delete",
"organizations.organizations.item.delete",
"organizations-storage.addresses.item.delete",
"organizations-storage.emails.item.delete",
diff --git a/src/Organizations/OrganizationCreate/OrganizationCreate.js b/src/Organizations/OrganizationCreate/OrganizationCreate.js
index 24f5d6de..9d39c782 100644
--- a/src/Organizations/OrganizationCreate/OrganizationCreate.js
+++ b/src/Organizations/OrganizationCreate/OrganizationCreate.js
@@ -1,19 +1,17 @@
-import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import { useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
+import { withRouter } from 'react-router-dom';
-import {
- stripesConnect,
-} from '@folio/stripes/core';
+import { stripesConnect } from '@folio/stripes/core';
import { useShowCallout } from '@folio/stripes-acq-components';
import { VIEW_ORG_DETAILS } from '../../common/constants';
import { organizationsResource } from '../../common/resources';
-import {
- OrganizationForm,
-} from '../OrganizationForm';
+import { BANKING_INFORMATION_FIELD_NAME } from '../constants';
import { handleSaveErrorResponse } from '../handleSaveErrorResponse';
+import { OrganizationForm } from '../OrganizationForm';
+import { useBankingInformationManager } from '../useBankingInformationManager';
const INITIAL_VALUES = {
interfaces: [],
@@ -32,6 +30,8 @@ const INITIAL_VALUES = {
};
export const OrganizationCreate = ({ history, location, mutator }) => {
+ const { manageBankingInformation } = useBankingInformationManager();
+
const cancelForm = useCallback(
(id) => {
history.push({
@@ -45,9 +45,21 @@ export const OrganizationCreate = ({ history, location, mutator }) => {
const showCallout = useShowCallout();
const intl = useIntl();
+
const createOrganization = useCallback(
- (data) => {
+ (values, { getFieldState }) => {
+ const { [BANKING_INFORMATION_FIELD_NAME]: bankingInformation, ...data } = values;
+
return mutator.createOrganizationOrg.POST(data)
+ .then(async organization => {
+ await manageBankingInformation({
+ initBankingInformation: getFieldState(BANKING_INFORMATION_FIELD_NAME)?.initial,
+ bankingInformation,
+ organization,
+ });
+
+ return organization;
+ })
.then(organization => {
setTimeout(() => cancelForm(organization.id));
showCallout({
@@ -60,12 +72,17 @@ export const OrganizationCreate = ({ history, location, mutator }) => {
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- [cancelForm, intl, showCallout],
+ [cancelForm, intl, manageBankingInformation, showCallout],
);
+ const initialValues = useMemo(() => ({
+ [BANKING_INFORMATION_FIELD_NAME]: [],
+ ...INITIAL_VALUES,
+ }), []);
+
return (
diff --git a/src/Organizations/OrganizationCreate/OrganizationCreate.test.js b/src/Organizations/OrganizationCreate/OrganizationCreate.test.js
index 1651446c..2ee3202c 100644
--- a/src/Organizations/OrganizationCreate/OrganizationCreate.test.js
+++ b/src/Organizations/OrganizationCreate/OrganizationCreate.test.js
@@ -1,13 +1,16 @@
-import React from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import { OrganizationForm } from '../OrganizationForm';
-
+import { useBankingInformationManager } from '../useBankingInformationManager';
import { OrganizationCreate } from './OrganizationCreate';
jest.mock('../OrganizationForm', () => ({
OrganizationForm: jest.fn().mockReturnValue('OrganizationForm'),
}));
+jest.mock('../useBankingInformationManager', () => ({
+ useBankingInformationManager: jest.fn(),
+}));
const mutatorMock = {
createOrganizationOrg: {
@@ -17,6 +20,18 @@ const mutatorMock = {
const historyMock = {
push: jest.fn(),
};
+
+const getFieldState = jest.fn();
+const manageBankingInformation = jest.fn();
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
const renderOrganizationCreate = (props) => render(
render(
mutator={mutatorMock}
{...props}
/>,
+ { wrapper },
);
describe('OrganizationCreate', () => {
beforeEach(() => {
OrganizationForm.mockClear();
+ getFieldState.mockClear();
historyMock.push.mockClear();
+ manageBankingInformation.mockClear();
mutatorMock.createOrganizationOrg.POST.mockClear();
+
+ useBankingInformationManager
+ .mockClear()
+ .mockReturnValue({ manageBankingInformation });
});
it('should display organization form', () => {
@@ -56,11 +78,23 @@ describe('OrganizationCreate', () => {
expect(historyMock.push.mock.calls[0][0].pathname).toBe('/organizations/view/orgUid');
});
- it('should save organization', () => {
+ it('should save organization', async () => {
mutatorMock.createOrganizationOrg.POST.mockReturnValue(Promise.resolve({ id: 'orgUid' }));
renderOrganizationCreate();
- OrganizationForm.mock.calls[0][0].onSubmit({});
+ await OrganizationForm.mock.calls[0][0].onSubmit({}, { getFieldState });
+
+ expect(mutatorMock.createOrganizationOrg.POST).toHaveBeenCalled();
+ });
+
+ it('should handle banking information on form submit', async () => {
+ mutatorMock.createOrganizationOrg.POST.mockReturnValue(Promise.resolve({ id: 'orgUid' }));
+
+ renderOrganizationCreate();
+
+ await OrganizationForm.mock.calls[0][0].onSubmit({}, { getFieldState });
+
+ expect(manageBankingInformation).toHaveBeenCalled();
});
});
diff --git a/src/Organizations/OrganizationEdit/OrganizationEdit.js b/src/Organizations/OrganizationEdit/OrganizationEdit.js
index 326bef8a..ea531813 100644
--- a/src/Organizations/OrganizationEdit/OrganizationEdit.js
+++ b/src/Organizations/OrganizationEdit/OrganizationEdit.js
@@ -1,6 +1,7 @@
-import React, {
+import {
useCallback,
useEffect,
+ useMemo,
useState,
} from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
@@ -19,27 +20,35 @@ import {
} from '@folio/stripes-acq-components';
import { VIEW_ORG_DETAILS } from '../../common/constants';
+import { useOrganizationBankingInformation } from '../../common/hooks';
import { organizationResourceByUrl } from '../../common/resources';
-import {
- OrganizationForm,
-} from '../OrganizationForm';
+import { BANKING_INFORMATION_FIELD_NAME } from '../constants';
+import { OrganizationForm } from '../OrganizationForm';
import { handleSaveErrorResponse } from '../handleSaveErrorResponse';
+import { useBankingInformationManager } from '../useBankingInformationManager';
export const OrganizationEdit = ({ match, history, location, mutator }) => {
const organizationId = match.params.id;
const [organization, setOrganization] = useState({});
- const [isLoading, setIsLoading] = useState(true);
+ const [isOrganizationLoading, setIsOrganizationLoading] = useState(true);
const showCallout = useShowCallout();
const intl = useIntl();
+ const { manageBankingInformation } = useBankingInformationManager();
+
+ const {
+ bankingInformation: bankingInformationData,
+ isLoading: isBankingInformationLoading,
+ } = useOrganizationBankingInformation(organizationId);
+
useEffect(
() => {
mutator.editOrganizationOrg.GET()
.then(organizationsResponse => {
setOrganization(organizationsResponse);
})
- .finally(() => setIsLoading(false));
+ .finally(() => setIsOrganizationLoading(false));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
@@ -58,8 +67,17 @@ export const OrganizationEdit = ({ match, history, location, mutator }) => {
);
const updateOrganization = useCallback(
- (data) => {
+ (values, { getFieldState }) => {
+ const { [BANKING_INFORMATION_FIELD_NAME]: bankingInformation, ...data } = values;
+
return mutator.editOrganizationOrg.PUT(data)
+ .then(() => {
+ return manageBankingInformation({
+ initBankingInformation: getFieldState(BANKING_INFORMATION_FIELD_NAME)?.initial,
+ bankingInformation,
+ organization: values,
+ });
+ })
.then(() => {
setTimeout(cancelForm);
showCallout({
@@ -72,9 +90,16 @@ export const OrganizationEdit = ({ match, history, location, mutator }) => {
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
- [cancelForm, intl, showCallout],
+ [cancelForm, intl, manageBankingInformation, showCallout],
);
+ const initialValues = useMemo(() => ({
+ [BANKING_INFORMATION_FIELD_NAME]: bankingInformationData,
+ ...organization,
+ }), [organization, bankingInformationData]);
+
+ const isLoading = isOrganizationLoading || isBankingInformationLoading;
+
if (isLoading) {
return (
@@ -85,7 +110,7 @@ export const OrganizationEdit = ({ match, history, location, mutator }) => {
return (
}
diff --git a/src/Organizations/OrganizationEdit/OrganizationEdit.test.js b/src/Organizations/OrganizationEdit/OrganizationEdit.test.js
index 5b7bd0cd..c5a9e576 100644
--- a/src/Organizations/OrganizationEdit/OrganizationEdit.test.js
+++ b/src/Organizations/OrganizationEdit/OrganizationEdit.test.js
@@ -1,18 +1,34 @@
-import React from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
+
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
+import { useOrganizationBankingInformation } from '../../common/hooks';
import { OrganizationForm } from '../OrganizationForm';
-
+import { useBankingInformationManager } from '../useBankingInformationManager';
import { OrganizationEdit } from './OrganizationEdit';
+jest.mock('../../common/hooks', () => ({
+ ...jest.requireActual('../../common/hooks'),
+ useOrganizationBankingInformation: jest.fn(),
+}));
jest.mock('../OrganizationForm', () => ({
OrganizationForm: jest.fn().mockReturnValue('OrganizationForm'),
}));
+jest.mock('../useBankingInformationManager', () => ({
+ useBankingInformationManager: jest.fn(),
+}));
const organization = {
name: 'Amazon',
id: 'orgUid',
};
+
+const bankingInformation = [{
+ id: 'banking-information-id',
+ bankName: 'bankName',
+ isPrimary: true,
+}];
+
const mutatorMock = {
editOrganizationOrg: {
GET: jest.fn(),
@@ -27,6 +43,18 @@ const matchMock = {
id: organization.id,
},
};
+
+const getFieldState = jest.fn();
+const manageBankingInformation = jest.fn();
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
const renderOrganizationEdit = (props) => render(
render(
mutator={mutatorMock}
{...props}
/>,
+ { wrapper },
);
describe('OrganizationEdit', () => {
beforeEach(() => {
OrganizationForm.mockClear();
+ getFieldState.mockClear();
historyMock.push.mockClear();
mutatorMock.editOrganizationOrg.GET.mockClear().mockReturnValue(Promise.resolve(organization));
mutatorMock.editOrganizationOrg.PUT.mockClear();
+
+ useOrganizationBankingInformation
+ .mockClear()
+ .mockReturnValue({ bankingInformation, isLoading: false });
+ useBankingInformationManager
+ .mockClear()
+ .mockReturnValue({ manageBankingInformation });
});
it('should display organization form', async () => {
@@ -70,7 +107,19 @@ describe('OrganizationEdit', () => {
renderOrganizationEdit();
await screen.findByText('OrganizationForm');
+ await OrganizationForm.mock.calls[0][0].onSubmit({}, { getFieldState });
+
+ expect(mutatorMock.editOrganizationOrg.PUT).toHaveBeenCalled();
+ });
+
+ it('should handle banking information on form submit', async () => {
+ mutatorMock.editOrganizationOrg.PUT.mockReturnValue(Promise.resolve({ id: 'orgUid' }));
+
+ renderOrganizationEdit();
+
+ await screen.findByText('OrganizationForm');
+ await OrganizationForm.mock.calls[0][0].onSubmit({}, { getFieldState });
- OrganizationForm.mock.calls[0][0].onSubmit({});
+ expect(manageBankingInformation).toHaveBeenCalled();
});
});
diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js
new file mode 100644
index 00000000..128113cd
--- /dev/null
+++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.js
@@ -0,0 +1,128 @@
+import PropTypes from 'prop-types';
+import { useMemo } from 'react';
+import { Field, useForm } from 'react-final-form';
+import { FormattedMessage } from 'react-intl';
+
+import {
+ Card,
+ Col,
+ Row,
+ TextField,
+} from '@folio/stripes/components';
+import { FieldSelectionFinal } from '@folio/stripes-acq-components';
+
+import { FieldIsPrimary } from '../../../../common/components';
+import { getAddressCategoryIdsSet } from '../../getAddressCategoryIdsSet';
+
+export const BankingInformationField = ({
+ bankingAccountTypeOptions,
+ categories,
+ fields,
+ index,
+ name,
+}) => {
+ const { getFieldState } = useForm();
+
+ const initCategoryId = getFieldState(`${name}.categoryId`)?.initial;
+ const addresses = getFieldState('addresses')?.value;
+ const addressCategoryIdsSet = useMemo(() => {
+ return getAddressCategoryIdsSet(addresses);
+ }, [addresses]);
+
+ const cardHeader = (
+
+ );
+
+ const categoriesOptions = useMemo(() => {
+ return categories.reduce((acc, { id, value }) => {
+ if (addressCategoryIdsSet.has(id) || id === initCategoryId) {
+ acc.push({ label: value, value: id });
+ }
+
+ return acc;
+ }, []);
+ }, [addressCategoryIdsSet, categories, initCategoryId]);
+
+ return (
+
+
+
+ }
+ name={`${name}.bankName`}
+ component={TextField}
+ fullWidth
+ validateFields={[]}
+ />
+
+
+ }
+ name={`${name}.bankAccountNumber`}
+ component={TextField}
+ fullWidth
+ validateFields={[]}
+ />
+
+
+ }
+ name={`${name}.transitNumber`}
+ component={TextField}
+ fullWidth
+ validateFields={[]}
+ />
+
+
+ }
+ name={`${name}.categoryId`}
+ dataOptions={categoriesOptions}
+ validateFields={[]}
+ />
+
+
+ }
+ name={`${name}.accountTypeId`}
+ dataOptions={bankingAccountTypeOptions}
+ fullWidth
+ validateFields={[]}
+ />
+
+
+ }
+ name={`${name}.notes`}
+ id={`${name}.notes`}
+ component={TextField}
+ fullWidth
+ validateFields={[]}
+ />
+
+
+
+ );
+};
+
+BankingInformationField.propTypes = {
+ bankingAccountTypeOptions: PropTypes.arrayOf(PropTypes.shape({
+ label: PropTypes.string,
+ value: PropTypes.string,
+ })).isRequired,
+ categories: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string,
+ value: PropTypes.string,
+ })).isRequired,
+ fields: PropTypes.object.isRequired,
+ index: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+};
diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.test.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.test.js
new file mode 100644
index 00000000..32af4dea
--- /dev/null
+++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/BankingInformationField.test.js
@@ -0,0 +1,42 @@
+import { Form } from 'react-final-form';
+
+import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
+
+import { BankingInformationField } from './BankingInformationField';
+
+const defaultProps = {
+ bankingAccountTypeOptions: [],
+ categories: [],
+ fields: { forEach: jest.fn },
+ index: 3,
+ name: 'bankingInfo[3]',
+};
+
+const wrapper = ({ children }) => (
+ );
+
+const wrapper = ({ children }) => (
+
+
+ {children}
+
+
+);
+
+const renderBankingInformationFieldArray = (props = {}) => render(
+ ,
+ { wrapper },
+);
+
+describe('OrganizationBankingInfoForm', () => {
+ it('should render RepeatableFieldWithValidation', async () => {
+ renderBankingInformationFieldArray();
+
+ expect(screen.getByText('RepeatableFieldWithValidation')).toBeInTheDocument();
+ });
+});
diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/index.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/index.js
new file mode 100644
index 00000000..84e0f493
--- /dev/null
+++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/index.js
@@ -0,0 +1 @@
+export { BankingInformationFieldArray } from './BankingInformationFieldArray';
diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.js
new file mode 100644
index 00000000..62e253ca
--- /dev/null
+++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.js
@@ -0,0 +1,68 @@
+import { useMemo } from 'react';
+import { FieldArray } from 'react-final-form-arrays';
+import { FormattedMessage } from 'react-intl';
+
+import { Loading } from '@folio/stripes/components';
+
+import {
+ useBankingAccountTypes,
+ useCategories,
+} from '../../../common/hooks';
+import {
+ createAddNewItem,
+ removeItem,
+} from '../../../common/utils';
+import { validatePrimary } from '../../../common/validation';
+import { BANKING_INFORMATION_FIELD_NAME } from '../../constants';
+import { BankingInformationField } from './BankingInformationField';
+import { BankingInformationFieldArray } from './BankingInformationFieldArray';
+
+const renderField = (props) => (name, index, fields) => (
+
+);
+
+export const OrganizationBankingInfoForm = () => {
+ const {
+ bankingAccountTypes,
+ isFetching: isBankingAccountTypesFetching,
+ } = useBankingAccountTypes();
+
+ const {
+ categories,
+ isFetching: isCategoriesFetching,
+ } = useCategories();
+
+ const bankingAccountTypeOptions = useMemo(() => {
+ return bankingAccountTypes.map(({ id, name }) => ({
+ label: name,
+ value: id,
+ }));
+ }, [bankingAccountTypes]);
+
+ const isLoading = isBankingAccountTypesFetching || isCategoriesFetching;
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+ }
+ component={BankingInformationFieldArray}
+ id="bankingInformation"
+ name={BANKING_INFORMATION_FIELD_NAME}
+ onAdd={createAddNewItem()}
+ onRemove={removeItem}
+ renderField={renderField({
+ bankingAccountTypeOptions,
+ categories,
+ })}
+ validate={validatePrimary}
+ />
+ );
+};
diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js
new file mode 100644
index 00000000..c3a5d269
--- /dev/null
+++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/OrganizationBankingInfoForm.test.js
@@ -0,0 +1,126 @@
+import { FieldArray } from 'react-final-form-arrays';
+import { MemoryRouter } from 'react-router-dom';
+
+import user from '@folio/jest-config-stripes/testing-library/user-event';
+import {
+ act,
+ render,
+ renderHook,
+ screen,
+ within,
+} from '@folio/jest-config-stripes/testing-library/react';
+import stripesFinalForm from '@folio/stripes/final-form';
+import { RepeatableFieldWithValidation } from '@folio/stripes-acq-components';
+
+import { EVENT_EMITTER_EVENTS } from '../../../common/constants';
+import {
+ useBankingAccountTypes,
+ useCategories,
+ useEventEmitter,
+} from '../../../common/hooks';
+import CategoryDropdown from '../../../Utils/CategoryDropdown';
+import { OrganizationBankingInfoForm } from './OrganizationBankingInfoForm';
+
+jest.mock('../../../common/hooks', () => ({
+ ...jest.requireActual('../../../common/hooks'),
+ useBankingAccountTypes: jest.fn(),
+ useCategories: jest.fn(),
+}));
+
+let form;
+
+const bankingAccountTypes = [
+ { id: '1', name: 'First banking account type' },
+ { id: '2', name: 'Second banking account type' },
+];
+const categories = [
+ { id: '1', value: 'First category' },
+ { id: '2', value: 'Second category' },
+];
+const addresses = [{ id: 'address-id', categories: categories.map(({ id }) => id) }];
+const initialFormValues = { addresses };
+
+const FormWrapper = stripesFinalForm({
+ keepDirtyOnReinitialize: true,
+ navigationCheck: true,
+ subscription: { values: true },
+ validateOnBlur: true,
+})(({ children, ...props }) => {
+ form = props.form;
+
+ return ;
+});
+
+const renderOrganizationBankingInfoForm = (props = {}, initialValues = initialFormValues) => render(
+
+ <>
+ (
+
+ )}
+ />
+
+ >
+ ,
+ { wrapper: MemoryRouter },
+);
+
+const addField = async () => user.click(await screen.findByRole('button', { name: 'ui-organizations.button.bankingInformation.add' }));
+
+describe('OrganizationBankingInfoForm', () => {
+ beforeEach(() => {
+ useBankingAccountTypes
+ .mockClear()
+ .mockReturnValue({ isFetching: false, bankingAccountTypes });
+ useCategories
+ .mockClear()
+ .mockReturnValue({ isFetching: false, categories });
+ });
+
+ it('should provide banking account types options for the related field', async () => {
+ renderOrganizationBankingInfoForm();
+
+ await addField();
+
+ bankingAccountTypes.forEach(({ name }) => {
+ expect(screen.getByText(name)).toBeInTheDocument();
+ });
+ });
+
+ describe('Interaction with \'Addresses\' categories fields', () => {
+ it('should render categories options based on selected address categories', async () => {
+ renderOrganizationBankingInfoForm();
+
+ await addField();
+
+ categories.forEach(({ value }) => {
+ expect(within(screen.getByTestId('banking-information-card')).getByText(value)).toBeInTheDocument();
+ });
+ });
+
+ it('should handle change address categories event', async () => {
+ const { result } = renderHook(() => useEventEmitter());
+
+ renderOrganizationBankingInfoForm();
+
+ act(() => {
+ form.change('addresses[0].categories', []);
+ result.current.emit(EVENT_EMITTER_EVENTS.ADDRESS_CATEGORY_CHANGED);
+ });
+
+ await addField();
+
+ categories.forEach(({ value }) => {
+ expect(within(screen.getByTestId('banking-information-card')).queryByText(value)).not.toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/index.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/index.js
new file mode 100644
index 00000000..0538aaeb
--- /dev/null
+++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/index.js
@@ -0,0 +1 @@
+export { OrganizationBankingInfoForm } from './OrganizationBankingInfoForm';
diff --git a/src/Organizations/OrganizationForm/OrganizationContactInfoForm/__snapshots__/OrganizationContactInfoForm.test.js.snap b/src/Organizations/OrganizationForm/OrganizationContactInfoForm/__snapshots__/OrganizationContactInfoForm.test.js.snap
index 387065b3..b527f1b4 100644
--- a/src/Organizations/OrganizationForm/OrganizationContactInfoForm/__snapshots__/OrganizationContactInfoForm.test.js.snap
+++ b/src/Organizations/OrganizationForm/OrganizationContactInfoForm/__snapshots__/OrganizationContactInfoForm.test.js.snap
@@ -299,7 +299,7 @@ exports[`OrganizationContactInfoForm should render correct structure with define
class="optionSegment"
data-test-selection-option-segment="true"
>
- stripes-components.countries.US
+ US
@@ -332,7 +332,7 @@ exports[`OrganizationContactInfoForm should render correct structure with define
class="selectionFilterContainer"
>
- stripes-components.countries.AF
+ AD
- stripes-components.countries.AX
+ AE
- stripes-components.countries.AL
+ AF
- stripes-components.countries.DZ
+ AG
- stripes-components.countries.AS
+ AI
- stripes-components.countries.AD
+ AL
- stripes-components.countries.AO
+ AM
- stripes-components.countries.AI
+ AN
- stripes-components.countries.AQ
+ AO
- stripes-components.countries.AG
+ AQ
- stripes-components.countries.AR
+ AR
- stripes-components.countries.AM
+ AS
- stripes-components.countries.AW
+ AT
- stripes-components.countries.AU
+ AU
- stripes-components.countries.AT
+ AW
- stripes-components.countries.AZ
+ AX
- stripes-components.countries.BS
+ AZ
- stripes-components.countries.BH
+ BA
- stripes-components.countries.BD
+ BB
- stripes-components.countries.BB
+ BD
- stripes-components.countries.BY
+ BE
- stripes-components.countries.BE
+ BF
- stripes-components.countries.BZ
+ BG
- stripes-components.countries.BJ
+ BH
- stripes-components.countries.BM
+ BI
- stripes-components.countries.BT
+ BJ
- stripes-components.countries.BO
+ BL
- stripes-components.countries.BA
+ BM
- stripes-components.countries.BW
+ BN
- stripes-components.countries.BV
+ BO
- stripes-components.countries.BR
+ BR
- stripes-components.countries.VG
+ BS
- stripes-components.countries.IO
+ BT
- stripes-components.countries.BN
+ BV
- stripes-components.countries.BG
+ BW
- stripes-components.countries.BF
+ BY
- stripes-components.countries.BI
+ BZ
- stripes-components.countries.KH
+ CA
- stripes-components.countries.CM
+ CC
- stripes-components.countries.CA
+ CD
- stripes-components.countries.CV
+ CF
- stripes-components.countries.KY
+ CG
- stripes-components.countries.CF
+ CH
- stripes-components.countries.TD
+ CI
- stripes-components.countries.CL
+ CK
- stripes-components.countries.CN
+ CL
- stripes-components.countries.HK
+ CM
- stripes-components.countries.MO
+ CN
- stripes-components.countries.CX
+ CO
- stripes-components.countries.CC
+ CR
- stripes-components.countries.CO
+ CU
- stripes-components.countries.KM
+ CV
- stripes-components.countries.CG
+ CX
- stripes-components.countries.CD
+ CY
- stripes-components.countries.CK
+ CZ
- stripes-components.countries.CR
+ DE
- stripes-components.countries.CI
+ DJ
- stripes-components.countries.HR
+ DK
- stripes-components.countries.CU
+ DM
- stripes-components.countries.CY
+ DO
- stripes-components.countries.CZ
+ DZ
- stripes-components.countries.DK
+ EC
- stripes-components.countries.DJ
+ EE
- stripes-components.countries.DM
+ EG
- stripes-components.countries.DO
+ EH
- stripes-components.countries.EC
+ ER
- stripes-components.countries.EG
+ ES
- stripes-components.countries.SV
+ ET
- stripes-components.countries.GQ
+ FI
- stripes-components.countries.ER
+ FJ
- stripes-components.countries.EE
+ FK
- stripes-components.countries.ET
+ FM
- stripes-components.countries.FK
+ FO
- stripes-components.countries.FO
+ FR
- stripes-components.countries.FJ
+ GA
- stripes-components.countries.FI
+ GB
- stripes-components.countries.FR
+ GD
- stripes-components.countries.GF
+ GE
- stripes-components.countries.PF
+ GF
- stripes-components.countries.TF
+ GG
- stripes-components.countries.GA
+ GH
- stripes-components.countries.GM
+ GI
- stripes-components.countries.GE
+ GL
- stripes-components.countries.DE
+ GM
- stripes-components.countries.GH
+ GN
- stripes-components.countries.GI
+ GP
- stripes-components.countries.GR
+ GQ
- stripes-components.countries.GL
+ GR
- stripes-components.countries.GD
+ GS
- stripes-components.countries.GP
+ GT
- stripes-components.countries.GU
+ GU
- stripes-components.countries.GT
+ GW
- stripes-components.countries.GG
+ GY
- stripes-components.countries.GN
+ HK
- stripes-components.countries.GW
+ HM
- stripes-components.countries.GY
+ HN
- stripes-components.countries.HT
+ HR
- stripes-components.countries.HM
+ HT
- stripes-components.countries.VA
+ HU
- stripes-components.countries.HN
+ ID
- stripes-components.countries.HU
+ IE
- stripes-components.countries.IS
+ IL
- stripes-components.countries.IN
+ IM
- stripes-components.countries.ID
+ IN
- stripes-components.countries.IR
+ IO
- stripes-components.countries.IQ
+ IQ
- stripes-components.countries.IE
+ IR
- stripes-components.countries.IM
+ IS
- stripes-components.countries.IL
+ IT
- stripes-components.countries.IT
+ JE
- stripes-components.countries.JM
+ JM
- stripes-components.countries.JP
+ JO
- stripes-components.countries.JE
+ JP
- stripes-components.countries.JO
+ KE
- stripes-components.countries.KZ
+ KG
- stripes-components.countries.KE
+ KH
- stripes-components.countries.KI
+ KI
- stripes-components.countries.KP
+ KM
- stripes-components.countries.KR
+ KN
- stripes-components.countries.KW
+ KP
- stripes-components.countries.KG
+ KR
- stripes-components.countries.LA
+ KW
- stripes-components.countries.LV
+ KY
- stripes-components.countries.LB
+ KZ
- stripes-components.countries.LS
+ LA
- stripes-components.countries.LR
+ LB
- stripes-components.countries.LY
+ LC
- stripes-components.countries.LI
+ LI
- stripes-components.countries.LT
+ LK
- stripes-components.countries.LU
+ LR
- stripes-components.countries.MK
+ LS
- stripes-components.countries.MG
+ LT
- stripes-components.countries.MW
+ LU
- stripes-components.countries.MY
+ LV
- stripes-components.countries.MV
+ LY
- stripes-components.countries.ML
+ MA
- stripes-components.countries.MT
+ MC
- stripes-components.countries.MH
+ MD
- stripes-components.countries.MQ
+ ME
- stripes-components.countries.MR
+ MF
- stripes-components.countries.MU
+ MG
- stripes-components.countries.YT
+ MH
- stripes-components.countries.MX
+ MK
- stripes-components.countries.FM
+ ML
- stripes-components.countries.MD
+ MM
- stripes-components.countries.MC
+ MN
- stripes-components.countries.MN
+ MO
- stripes-components.countries.ME
+ MP
- stripes-components.countries.MS
+ MQ
- stripes-components.countries.MA
+ MR
- stripes-components.countries.MZ
+ MS
- stripes-components.countries.MM
+ MT
- stripes-components.countries.NA
+ MU
- stripes-components.countries.NR
+ MV
- stripes-components.countries.NP
+ MW
- stripes-components.countries.NL
+ MX
- stripes-components.countries.AN
+ MY
- stripes-components.countries.NC
+ MZ
- stripes-components.countries.NZ
+ NA
- stripes-components.countries.NI
+ NC
- stripes-components.countries.NE
+ NE
- stripes-components.countries.NG
+ NF
- stripes-components.countries.NU
+ NG
- stripes-components.countries.NF
+ NI
- stripes-components.countries.MP
+ NL
- stripes-components.countries.NO
+ NO
- stripes-components.countries.OM
+ NP
- stripes-components.countries.PK
+ NR
- stripes-components.countries.PW
+ NU
- stripes-components.countries.PS
+ NZ
- stripes-components.countries.PA
+ OM
- stripes-components.countries.PG
+ PA
- stripes-components.countries.PY
+ PE
- stripes-components.countries.PE
+ PF
- stripes-components.countries.PH
+ PG
- stripes-components.countries.PN
+ PH
- stripes-components.countries.PL
+ PK
- stripes-components.countries.PT
+ PL
- stripes-components.countries.PR
+ PM
- stripes-components.countries.QA
+ PN
- stripes-components.countries.RE
+ PR
- stripes-components.countries.RO
+ PS
- stripes-components.countries.RU
+ PT
- stripes-components.countries.RW
+ PW
- stripes-components.countries.BL
+ PY
- stripes-components.countries.SH
+ QA
- stripes-components.countries.KN
+ RE
- stripes-components.countries.LC
+ RO
- stripes-components.countries.MF
+ RS
- stripes-components.countries.PM
+ RU
- stripes-components.countries.VC
+ RW
- stripes-components.countries.WS
+ SA
- stripes-components.countries.SM
+ SB
- stripes-components.countries.ST
+ SC
- stripes-components.countries.SA
+ SD
- stripes-components.countries.SN
+ SE
- stripes-components.countries.RS
+ SG
- stripes-components.countries.SC
+ SH
- stripes-components.countries.SL
+ SI
- stripes-components.countries.SG
+ SJ
- stripes-components.countries.SK
+ SK
- stripes-components.countries.SI
+ SL
- stripes-components.countries.SB
+ SM
- stripes-components.countries.SO
+ SN
- stripes-components.countries.ZA
+ SO
- stripes-components.countries.GS
+ SR
- stripes-components.countries.SS
+ SS
- stripes-components.countries.ES
+ ST
- stripes-components.countries.LK
+ SV
- stripes-components.countries.SD
+ SY
- stripes-components.countries.SR
+ SZ
- stripes-components.countries.SJ
+ TC
- stripes-components.countries.SZ
+ TD
- stripes-components.countries.SE
+ TF
- stripes-components.countries.CH
+ TG
- stripes-components.countries.SY
+ TH
- stripes-components.countries.TW
+ TJ
- stripes-components.countries.TJ
+ TK
- stripes-components.countries.TZ
+ TL
- stripes-components.countries.TH
+ TM
- stripes-components.countries.TL
+ TN
- stripes-components.countries.TG
+ TO
- stripes-components.countries.TK
+ TR
- stripes-components.countries.TO
+ TT
- stripes-components.countries.TT
+ TV
- stripes-components.countries.TN
+ TW
- stripes-components.countries.TR
+ TZ
- stripes-components.countries.TM
+ UA
- stripes-components.countries.TC
+ UG
- stripes-components.countries.TV
+ UM
- stripes-components.countries.UG
+ US
- stripes-components.countries.UA
+ UY
- stripes-components.countries.AE
+ UZ
- stripes-components.countries.GB
+ VA
- stripes-components.countries.US
+ VC
- stripes-components.countries.UM
+ VE
- stripes-components.countries.UY
+ VG
- stripes-components.countries.UZ
+ VI
- stripes-components.countries.VU
+ VN
- stripes-components.countries.VE
+ VU
- stripes-components.countries.VN
+ WF
- stripes-components.countries.VI
+ WS
- stripes-components.countries.WF
+ YE
- stripes-components.countries.EH
+ YT
- stripes-components.countries.YE
+ ZA
- stripes-components.countries.ZM
+ ZM
- stripes-components.countries.ZW
+ ZW
diff --git a/src/Organizations/OrganizationForm/OrganizationForm.js b/src/Organizations/OrganizationForm/OrganizationForm.js
index e1d6bd4d..11826ddb 100644
--- a/src/Organizations/OrganizationForm/OrganizationForm.js
+++ b/src/Organizations/OrganizationForm/OrganizationForm.js
@@ -1,4 +1,3 @@
-import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router';
@@ -24,6 +23,8 @@ import {
} from '@folio/stripes-acq-components';
import { ORGANIZATIONS_ROUTE } from '../../common/constants';
+import { useBankingInformationSettings } from '../../common/hooks';
+import { OrganizationBankingInfoForm } from './OrganizationBankingInfoForm';
import { OrganizationSummaryForm } from './OrganizationSummaryForm';
import { OrganizationContactInfoFormContainer } from './OrganizationContactInfoForm';
import { OrganizationContactPeopleForm } from './OrganizationContactPeopleForm';
@@ -54,6 +55,7 @@ const OrganizationForm = ({
[ORGANIZATION_SECTIONS.contactPeopleSection]: false,
[ORGANIZATION_SECTIONS.interfacesSection]: false,
[ORGANIZATION_SECTIONS.vendorInformationSection]: false,
+ [ORGANIZATION_SECTIONS.bankingInformationSection]: false,
[ORGANIZATION_SECTIONS.vendorTermsSection]: false,
[ORGANIZATION_SECTIONS.accountsSection]: false,
};
@@ -69,6 +71,9 @@ const OrganizationForm = ({
: stateSections;
const history = useHistory();
const { id, interfaces, contacts, metadata } = initialValues;
+
+ const { enabled: isBankingInformationEnabled } = useBankingInformationSettings();
+
const shortcuts = [
{
name: 'cancel',
@@ -216,6 +221,15 @@ const OrganizationForm = ({
accountFormValues={formValues.accounts}
/>
+
+ {isBankingInformationEnabled && (
+
+
+
+ )}
>
)
}
diff --git a/src/Organizations/OrganizationForm/OrganizationForm.test.js b/src/Organizations/OrganizationForm/OrganizationForm.test.js
index 71c527bd..5a031cbf 100644
--- a/src/Organizations/OrganizationForm/OrganizationForm.test.js
+++ b/src/Organizations/OrganizationForm/OrganizationForm.test.js
@@ -1,3 +1,4 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
import { MemoryRouter } from 'react-router-dom';
import { useHistory } from 'react-router';
@@ -16,6 +17,7 @@ import user from '@folio/jest-config-stripes/testing-library/user-event';
import { organizationTypes } from 'fixtures';
import { ORGANIZATIONS_ROUTE } from '../../common/constants';
+import { useBankingInformationSettings } from '../../common/hooks';
import OrganizationForm from './OrganizationForm';
jest.mock('react-router', () => ({
@@ -27,6 +29,10 @@ jest.mock('@folio/stripes-components/lib/Commander', () => ({
expandAllSections: jest.fn(),
collapseAllSections: jest.fn(),
}));
+jest.mock('../../common/hooks', () => ({
+ ...jest.requireActual('../../common/hooks'),
+ useBankingInformationSettings: jest.fn(),
+}));
jest.mock(
'./OrganizationSummaryForm',
() => ({ OrganizationSummaryForm: () => 'OrganizationSummaryForm' }),
@@ -55,6 +61,9 @@ jest.mock(
'./OrganizationAccountsForm',
() => ({ OrganizationAccountsForm: () => 'OrganizationAccountsForm' }),
);
+jest.mock('./OrganizationBankingInfoForm', () => ({
+ OrganizationBankingInfoForm: () => 'OrganizationBankingInfoForm',
+}));
const queryAllByClass = queryHelpers.queryAllByAttribute.bind(null, 'class');
@@ -68,14 +77,27 @@ const defaultProps = {
organizationTypes: organizationTypesMock,
cancelForm: jest.fn(),
};
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+
+ {children}
+
+
+);
+
const renderOrganizationForm = (props = defaultProps) => render(
,
- { wrapper: MemoryRouter },
+ { wrapper },
);
describe('OrganizationForm', () => {
beforeEach(() => {
global.document.createRange = global.document.originalCreateRange;
+
+ useBankingInformationSettings.mockClear().mockReturnValue({ enabled: false });
});
afterEach(() => {
@@ -155,6 +177,17 @@ describe('OrganizationForm', () => {
.length,
).toBe(sections.length);
});
+
+ it('should render banking information form', () => {
+ useBankingInformationSettings.mockReturnValue({ enabled: true });
+
+ renderOrganizationForm({
+ ...defaultProps,
+ initialValues: { isVendor: true },
+ });
+
+ expect(screen.getByText('OrganizationBankingInfoForm')).toBeInTheDocument();
+ });
});
describe('Shortcuts', () => {
diff --git a/src/Organizations/OrganizationForm/getAddressCategoryIdsSet.js b/src/Organizations/OrganizationForm/getAddressCategoryIdsSet.js
new file mode 100644
index 00000000..d10209e6
--- /dev/null
+++ b/src/Organizations/OrganizationForm/getAddressCategoryIdsSet.js
@@ -0,0 +1,9 @@
+import memoize from 'lodash/memoize';
+
+export const getAddressCategoryIdsSet = memoize((addresses = []) => {
+ return addresses.reduce((acc, address) => {
+ address.categories?.forEach(categoryId => acc.add(categoryId));
+
+ return acc;
+ }, new Set());
+});
diff --git a/src/Organizations/OrganizationForm/getAddressCategoryIdsSet.test.js b/src/Organizations/OrganizationForm/getAddressCategoryIdsSet.test.js
new file mode 100644
index 00000000..3f5b157b
--- /dev/null
+++ b/src/Organizations/OrganizationForm/getAddressCategoryIdsSet.test.js
@@ -0,0 +1,13 @@
+import { getAddressCategoryIdsSet } from './getAddressCategoryIdsSet';
+
+describe('getAddressCategoryIdsSet', () => {
+ it('should return a set of addresses categories', () => {
+ const addresses = [
+ { categories: [1, 2, 3] },
+ { categories: [2, 3, 4] },
+ { categories: [1, 4, 5] },
+ ];
+
+ expect(getAddressCategoryIdsSet(addresses).size).toEqual(5);
+ });
+});
diff --git a/src/Organizations/constants.js b/src/Organizations/constants.js
index 457a9a5d..717e9b55 100644
--- a/src/Organizations/constants.js
+++ b/src/Organizations/constants.js
@@ -1,8 +1,8 @@
-import React from 'react';
import { FormattedMessage } from 'react-intl';
export const ORGANIZATION_SECTIONS = {
summarySection: 'summarySection',
+ bankingInformationSection: 'bankingInformationSection',
contactInformationSection: 'contactInformationSection',
contactPeopleSection: 'contactPeopleSection',
interfacesSection: 'interfacesSection',
@@ -16,6 +16,7 @@ export const ORGANIZATION_SECTIONS = {
export const ORGANIZATION_SECTION_LABELS = {
[ORGANIZATION_SECTIONS.summarySection]: ,
+ [ORGANIZATION_SECTIONS.bankingInformationSection]: ,
[ORGANIZATION_SECTIONS.contactInformationSection]: ,
[ORGANIZATION_SECTIONS.contactPeopleSection]: ,
[ORGANIZATION_SECTIONS.interfacesSection]: ,
@@ -41,3 +42,5 @@ export const MAP_FIELD_ACCORDION = {
agreements: ORGANIZATION_SECTIONS.vendorTermsSection,
accounts: ORGANIZATION_SECTIONS.accountsSection,
};
+
+export const BANKING_INFORMATION_FIELD_NAME = 'bankingInformation';
diff --git a/src/Organizations/useBankingInformationManager.js b/src/Organizations/useBankingInformationManager.js
new file mode 100644
index 00000000..78a8ee85
--- /dev/null
+++ b/src/Organizations/useBankingInformationManager.js
@@ -0,0 +1,70 @@
+import chunk from 'lodash/chunk';
+import { useCallback } from 'react';
+
+import { useShowCallout } from '@folio/stripes-acq-components';
+
+import {
+ useBankingInformationMutation,
+ useBankingInformationSettings,
+} from '../common/hooks';
+import { getArrayItemsChanges } from '../common/utils';
+
+const executeSequentially = (fn, arr) => arr.reduce((acc, curr) => {
+ return acc.then(() => fn({ bankingInformation: curr }));
+}, Promise.resolve());
+
+const executeParallel = (fn, arr) => chunk(arr, 5).reduce((acc, chunked) => {
+ return acc.then(() => Promise.all(chunked.map((bankingInformation) => fn({ bankingInformation }))));
+}, Promise.resolve());
+
+export const useBankingInformationManager = () => {
+ const showCallout = useShowCallout();
+
+ const { enabled: isBankingInformationEnabled } = useBankingInformationSettings();
+
+ const {
+ createBankingInformation,
+ updateBankingInformation,
+ deleteBankingInformation,
+ isLoading,
+ } = useBankingInformationMutation();
+
+ const manageBankingInformation = useCallback(({
+ initBankingInformation,
+ bankingInformation,
+ organization,
+ }) => {
+ if (!(organization.isVendor && isBankingInformationEnabled)) return Promise.resolve();
+
+ const {
+ created,
+ updated,
+ deleted,
+ } = getArrayItemsChanges(initBankingInformation, bankingInformation);
+
+ return Promise.all([
+ executeSequentially(createBankingInformation, created.map((item) => ({
+ organizationId: organization.id,
+ ...item,
+ }))),
+ executeParallel(updateBankingInformation, updated),
+ executeParallel(deleteBankingInformation, deleted),
+ ]).catch(() => {
+ showCallout({
+ type: 'error',
+ messageId: 'ui-organizations.bankingInformation.save.error',
+ });
+ });
+ }, [
+ createBankingInformation,
+ deleteBankingInformation,
+ isBankingInformationEnabled,
+ showCallout,
+ updateBankingInformation,
+ ]);
+
+ return {
+ manageBankingInformation,
+ isLoading,
+ };
+};
diff --git a/src/Organizations/useBankingInformationManager.test.js b/src/Organizations/useBankingInformationManager.test.js
new file mode 100644
index 00000000..d88aa39b
--- /dev/null
+++ b/src/Organizations/useBankingInformationManager.test.js
@@ -0,0 +1,103 @@
+import { renderHook } from '@folio/jest-config-stripes/testing-library/react';
+
+import { organization } from 'fixtures';
+import {
+ useBankingInformationMutation,
+ useBankingInformationSettings,
+} from '../common/hooks';
+import { useBankingInformationManager } from './useBankingInformationManager';
+
+const mutationsMock = {
+ createBankingInformation: jest.fn(),
+ updateBankingInformation: jest.fn(),
+ deleteBankingInformation: jest.fn(),
+};
+
+jest.mock('../common/hooks', () => ({
+ ...jest.requireActual('../common/hooks'),
+ useBankingInformationMutation: jest.fn(),
+ useBankingInformationSettings: jest.fn(),
+}));
+
+const organizationId = organization.id;
+const initBankingInformation = [
+ { id: 1, bankName: 'Bank name 1', organizationId },
+ { id: 2, bankName: 'Bank name 2', organizationId },
+];
+const bankingInformation = [
+ { id: 1, bankName: 'Bank name 1 (Edited)', organizationId },
+ { id: 3, bankName: 'Bank name 3' },
+];
+
+describe('useBankingAccountTypes', () => {
+ beforeEach(() => {
+ Object
+ .values(mutationsMock)
+ .forEach(fn => fn.mockClear());
+ useBankingInformationMutation
+ .mockClear()
+ .mockReturnValue(mutationsMock);
+ useBankingInformationSettings
+ .mockClear()
+ .mockReturnValue({ enabled: true });
+ });
+
+ it('should handle banking information fields change', async () => {
+ const { result } = renderHook(() => useBankingInformationManager());
+ const { manageBankingInformation } = result.current;
+
+ await manageBankingInformation({
+ initBankingInformation,
+ bankingInformation,
+ organization,
+ });
+
+ expect(mutationsMock.createBankingInformation).toHaveBeenCalledWith({
+ bankingInformation: {
+ organizationId,
+ ...bankingInformation[1],
+ },
+ });
+ expect(mutationsMock.updateBankingInformation).toHaveBeenCalledWith({
+ bankingInformation: bankingInformation[0],
+ });
+ expect(mutationsMock.deleteBankingInformation).toHaveBeenCalledWith({
+ bankingInformation: initBankingInformation[1],
+ });
+ });
+
+ it('should return a resolved promise immediately if an organization is not a vendor', async () => {
+ const { result } = renderHook(() => useBankingInformationManager());
+ const { manageBankingInformation } = result.current;
+
+ await manageBankingInformation({
+ initBankingInformation,
+ bankingInformation,
+ organization: {
+ ...organization,
+ isVendor: false,
+ },
+ });
+
+ expect(mutationsMock.createBankingInformation).not.toHaveBeenCalled();
+ expect(mutationsMock.deleteBankingInformation).not.toHaveBeenCalled();
+ expect(mutationsMock.updateBankingInformation).not.toHaveBeenCalled();
+ });
+
+ it('should return a resolved promise immediately if the banking information settings are disabled', async () => {
+ useBankingInformationSettings.mockReturnValue({ enabled: false });
+
+ const { result } = renderHook(() => useBankingInformationManager());
+ const { manageBankingInformation } = result.current;
+
+ await manageBankingInformation({
+ initBankingInformation,
+ bankingInformation,
+ organization,
+ });
+
+ expect(mutationsMock.createBankingInformation).not.toHaveBeenCalled();
+ expect(mutationsMock.deleteBankingInformation).not.toHaveBeenCalled();
+ expect(mutationsMock.updateBankingInformation).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js b/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js
index 0ffb2568..14b78f0b 100644
--- a/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js
+++ b/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js
@@ -1,11 +1,10 @@
-import React from 'react';
import { FormattedMessage } from 'react-intl';
import { getControlledVocabTranslations } from '@folio/stripes-acq-components';
import { useStripes } from '@folio/stripes/core';
import { ControlledVocab } from '@folio/stripes/smart-components';
-import { BANKING_ACCOUNT_TYPES_API } from '../constants';
+import { BANKING_ACCOUNT_TYPES_API } from '../../common/constants';
const setUniqValidation = (value, index, items) => {
const errors = {};
diff --git a/src/Settings/BankingInformationSettings/BankingInformationSettings.js b/src/Settings/BankingInformationSettings/BankingInformationSettings.js
index 5c45c571..614124ac 100644
--- a/src/Settings/BankingInformationSettings/BankingInformationSettings.js
+++ b/src/Settings/BankingInformationSettings/BankingInformationSettings.js
@@ -4,26 +4,24 @@ import { useShowCallout } from '@folio/stripes-acq-components';
import { Loading } from '@folio/stripes/components';
import { useOkapiKy } from '@folio/stripes/core';
-import { SETTINGS_API } from '../constants';
-import { useBankingInformation } from '../hooks';
+import { SETTINGS_API } from '../../common/constants';
+import { useBankingInformationSettings } from '../../common/hooks';
import BankingInformationSettingsForm from './BankingInformationSettingsForm';
const BankingInformationSettings = () => {
const {
enabled,
- key,
- id: bankingInformationId,
- version,
+ bankingInformation,
isLoading,
refetch,
- } = useBankingInformation();
+ } = useBankingInformationSettings();
const ky = useOkapiKy();
const sendCallout = useShowCallout();
const onSubmit = async ({ value }) => {
try {
- await ky.put(`${SETTINGS_API}/${bankingInformationId}`, {
- json: { value, key, _version: version },
+ await ky.put(`${SETTINGS_API}/${bankingInformation.id}`, {
+ json: { ...bankingInformation, value },
});
refetch();
diff --git a/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js b/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js
index 4bacedac..60d4d71b 100644
--- a/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js
+++ b/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js
@@ -1,29 +1,20 @@
import { MemoryRouter } from 'react-router-dom';
-import {
- act,
- render,
- screen,
-} from '@folio/jest-config-stripes/testing-library/react';
+import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import user from '@folio/jest-config-stripes/testing-library/user-event';
import { useOkapiKy } from '@folio/stripes/core';
-import { useBankingInformation } from '../hooks';
+import { useBankingInformationSettings } from '../../common/hooks';
import BankingInformationSettings from './BankingInformationSettings';
-const mockRefetch = jest.fn();
-
jest.mock('@folio/stripes/components', () => ({
...jest.requireActual('@folio/stripes/components'),
Loading: () => Loading
,
}));
-jest.mock('../hooks', () => ({
- useBankingInformation: jest.fn(() => ({
- isLoading: false,
- enabled: false,
- refetch: mockRefetch,
- })),
+jest.mock('../../common/hooks', () => ({
+ ...jest.requireActual('../../common/hooks'),
+ useBankingInformationSettings: jest.fn(),
}));
const renderBankingInformationSettings = () => render(
@@ -31,7 +22,20 @@ const renderBankingInformationSettings = () => render(
{ wrapper: MemoryRouter },
);
+const mockRefetch = jest.fn();
+const bankingInformation = { id: 'banking-information-id' };
+
describe('BankingInformationSettings component', () => {
+ beforeEach(() => {
+ useBankingInformationSettings
+ .mockClear()
+ .mockReturnValue({
+ isLoading: false,
+ enabled: false,
+ refetch: mockRefetch,
+ });
+ });
+
it('should display pane headings', () => {
renderBankingInformationSettings();
@@ -43,7 +47,7 @@ describe('BankingInformationSettings component', () => {
});
it('should render Loading component', () => {
- useBankingInformation.mockReturnValue({
+ useBankingInformationSettings.mockReturnValue({
isLoading: true,
enabled: false,
});
@@ -54,7 +58,8 @@ describe('BankingInformationSettings component', () => {
});
it('should save banking options', async () => {
- useBankingInformation.mockClear().mockReturnValue({
+ useBankingInformationSettings.mockClear().mockReturnValue({
+ bankingInformation,
isLoading: false,
enabled: true,
refetch: mockRefetch,
@@ -71,13 +76,8 @@ describe('BankingInformationSettings component', () => {
renderBankingInformationSettings();
- const checkbox = screen.getByRole('checkbox', { name: 'ui-organizations.settings.bankingInformation.enable' });
- const saveButton = screen.getByText('ui-organizations.settings.accountTypes.save.button');
-
- await act(async () => {
- await user.click(checkbox);
- await user.click(saveButton);
- });
+ await user.click(await screen.findByRole('checkbox', { name: 'ui-organizations.settings.bankingInformation.enable' }));
+ await user.click(await screen.findByRole('button', { name: 'ui-organizations.settings.accountTypes.save.button' }));
expect(mockPutMethod).toHaveBeenCalled();
});
diff --git a/src/Settings/SettingsPage.js b/src/Settings/SettingsPage.js
index e422f927..5dd8ba63 100644
--- a/src/Settings/SettingsPage.js
+++ b/src/Settings/SettingsPage.js
@@ -1,14 +1,13 @@
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { Settings } from '@folio/stripes/smart-components';
-import { Loading } from '@folio/stripes/components';
-import { useBankingInformation } from './hooks';
+import { useBankingInformationSettings } from '../common/hooks';
import { CategorySettings } from './CategorySettings';
import { TypeSettings } from './TypeSettings';
-import { BankingInformationSettings } from './BankingInformationSettings';
import { BankingAccountTypeSettings } from './BankingAccountTypeSettings';
+import { BankingInformationSettings } from './BankingInformationSettings';
const pages = [
{
@@ -39,14 +38,10 @@ const bankingAccountTypesPage = {
};
const SettingsPage = (props) => {
- const { enabled, isLoading } = useBankingInformation();
+ const { enabled } = useBankingInformationSettings();
const settingsPages = useMemo(() => (enabled ? pages.concat(bankingAccountTypesPage) : pages), [enabled]);
- if (isLoading) {
- return ;
- }
-
return (
({
...jest.requireActual('@folio/stripes/components'),
Loading: () => Loading
,
}));
-jest.mock('./hooks', () => ({
- useBankingInformation: jest.fn(() => ({
+jest.mock('../common/hooks', () => ({
+ ...jest.requireActual('../common/hooks'),
+ useBankingInformationSettings: jest.fn(() => ({
isLoading: false,
enabled: false,
})),
@@ -52,7 +52,7 @@ describe('SettingsPage', () => {
});
it('should return banking account types link', async () => {
- useBankingInformation.mockReturnValue({
+ useBankingInformationSettings.mockReturnValue({
isLoading: false,
enabled: true,
});
@@ -61,15 +61,4 @@ describe('SettingsPage', () => {
expect(screen.getByText('ui-organizations.settings.bankingAccountTypes')).toBeInTheDocument();
});
-
- it('should display loading on fetching useBankingInformation', async () => {
- useBankingInformation.mockReturnValue({
- isLoading: true,
- enabled: false,
- });
-
- renderSettingsPage();
-
- expect(screen.getByText('Loading')).toBeInTheDocument();
- });
});
diff --git a/src/Settings/constants.js b/src/Settings/constants.js
deleted file mode 100644
index 56157dab..00000000
--- a/src/Settings/constants.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const SETTINGS_API = 'organizations-storage/settings';
-export const BANKING_ACCOUNT_TYPES_API = 'organizations-storage/banking-account-types';
-
-export const BANKING_INFORMATION_ENABLED_QUERY_KEY = 'BANKING_INFORMATION_ENABLED';
-export const BANKING_INFORMATION_SEARCH_PARAMS = {
- query: `key=${BANKING_INFORMATION_ENABLED_QUERY_KEY}`,
- limit: 1,
-};
diff --git a/src/Settings/hooks/index.js b/src/Settings/hooks/index.js
deleted file mode 100644
index 2144f705..00000000
--- a/src/Settings/hooks/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { useBankingInformation } from './useBankingInformation';
diff --git a/src/Settings/hooks/useBankingInformation.js b/src/Settings/hooks/useBankingInformation.js
deleted file mode 100644
index 23dce38d..00000000
--- a/src/Settings/hooks/useBankingInformation.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { get } from 'lodash';
-import { useQuery } from 'react-query';
-
-import {
- useNamespace,
- useOkapiKy,
-} from '@folio/stripes/core';
-
-import {
- BANKING_INFORMATION_SEARCH_PARAMS,
- SETTINGS_API,
-} from '../constants';
-
-export const useBankingInformation = () => {
- const ky = useOkapiKy();
- const [namespace] = useNamespace({ key: 'banking-information-settings' });
-
- const { isLoading, data, refetch } = useQuery(
- [namespace],
- () => ky.get(SETTINGS_API, {
- searchParams: BANKING_INFORMATION_SEARCH_PARAMS,
- }).json(),
- );
-
- const bankingInformation = get(data, 'settings[0]', {});
-
- return ({
- id: bankingInformation.id,
- enabled: bankingInformation.value === 'true',
- key: bankingInformation.key,
- version: bankingInformation._version,
- isLoading,
- refetch,
- });
-};
diff --git a/src/Utils/CategoryDropdown/CategoryDropdown.js b/src/Utils/CategoryDropdown/CategoryDropdown.js
index 43c73a2a..ba5b7803 100644
--- a/src/Utils/CategoryDropdown/CategoryDropdown.js
+++ b/src/Utils/CategoryDropdown/CategoryDropdown.js
@@ -1,6 +1,7 @@
-import React, { useCallback, useMemo } from 'react';
+import find from 'lodash/find';
import PropTypes from 'prop-types';
-import { find } from 'lodash';
+import { useCallback, useMemo } from 'react';
+import { useForm } from 'react-final-form';
import { FormattedMessage } from 'react-intl';
import { OptionSegment } from '@folio/stripes/components';
@@ -8,7 +9,15 @@ import { FieldMultiSelectionFinal } from '@folio/stripes-acq-components';
import { filterCategories } from './utils';
-function CategoryDropdown({ dropdownVendorCategories, name, withLabel, ariaLabelledBy }) {
+function CategoryDropdown({
+ dropdownVendorCategories,
+ name,
+ withLabel,
+ ariaLabelledBy,
+ onChange: onChangeProp,
+}) {
+ const { change } = useForm();
+
const fieldName = name ? `${name}.categories` : 'categories';
const toString = useCallback((option) => (
option ? `${fieldName}-${option}` : option
@@ -35,6 +44,12 @@ function CategoryDropdown({ dropdownVendorCategories, name, withLabel, ariaLabel
return dropdownVendorCategories.map(item => item.id) || [];
}, [dropdownVendorCategories]);
+ const onChange = useCallback((value) => {
+ change(fieldName, value);
+
+ if (onChangeProp) onChangeProp(value);
+ }, [change, fieldName, onChangeProp]);
+
return (
: undefined}
@@ -44,6 +59,7 @@ function CategoryDropdown({ dropdownVendorCategories, name, withLabel, ariaLabel
itemToString={toString}
formatter={formatter}
filter={filterItems}
+ onChange={onChange}
/>
);
}
@@ -53,6 +69,7 @@ CategoryDropdown.propTypes = {
name: PropTypes.string,
withLabel: PropTypes.bool,
ariaLabelledBy: PropTypes.string,
+ onChange: PropTypes.func,
};
CategoryDropdown.defaultProps = {
diff --git a/src/Utils/CategoryDropdown/CategoryDropdown.test.js b/src/Utils/CategoryDropdown/CategoryDropdown.test.js
index 6a401d64..bed45aee 100644
--- a/src/Utils/CategoryDropdown/CategoryDropdown.test.js
+++ b/src/Utils/CategoryDropdown/CategoryDropdown.test.js
@@ -1,23 +1,28 @@
-import React from 'react';
+import { Form } from 'react-final-form';
+import { MemoryRouter } from 'react-router-dom';
+
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import CategoryDropdown from './CategoryDropdown';
-jest.mock('react-final-form', () => ({
- // eslint-disable-next-line
- Field: ({ component, ...rest }) => {
- const Component = component;
-
- return ;
- },
-}));
-
const categories = [
{ id: 'category1', value: 'Main' },
{ id: 'category2', value: 'Uncategorize' },
];
-const renderCategoryDropdown = (props = {}) => render();
+const wrapper = ({ children }) => (
+
+
+);
+
+const renderCategoryDropdown = (props = {}) => render(
+ ,
+ { wrapper },
+);
describe('CategoryDropdown', () => {
it('should display passed categories as options', () => {
diff --git a/src/common/components/AddressInfo/AddressInfo.js b/src/common/components/AddressInfo/AddressInfo.js
index 625956dd..edcd9ab2 100644
--- a/src/common/components/AddressInfo/AddressInfo.js
+++ b/src/common/components/AddressInfo/AddressInfo.js
@@ -1,9 +1,9 @@
-import React from 'react';
+import PropTypes from 'prop-types';
+import { useCallback, useMemo } from 'react';
import {
FormattedMessage,
injectIntl,
} from 'react-intl';
-import PropTypes from 'prop-types';
import { Field } from 'react-final-form';
import { FieldArray } from 'react-final-form-arrays';
@@ -21,6 +21,8 @@ import {
} from '@folio/stripes-acq-components';
import CategoryDropdown from '../../../Utils/CategoryDropdown';
+import { EVENT_EMITTER_EVENTS } from '../../constants';
+import { useEventEmitter } from '../../hooks';
import {
createAddNewItem,
removeItem,
@@ -36,10 +38,20 @@ const AddressInfo = ({
dropdownVendorCategories,
intl,
}) => {
- const countriesOptions = countries.map(c => ({
- label: intl.formatMessage({ id: `stripes-components.countries.${c.alpha2}` }),
- value: c.alpha3,
- }));
+ const eventEmitter = useEventEmitter();
+
+ const onCategoryChange = useCallback(() => {
+ eventEmitter.emit(EVENT_EMITTER_EVENTS.ADDRESS_CATEGORY_CHANGED);
+ }, [eventEmitter]);
+
+ const countriesOptions = useMemo(() => (
+ countries
+ .map(c => ({
+ label: intl.formatDisplayName(c.alpha2, { type: 'region' }),
+ value: c.alpha3,
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label))
+ ), [intl]);
// eslint-disable-next-line react/prop-types
const Address = (name, index, fields) => {
@@ -161,6 +173,7 @@ const AddressInfo = ({
ariaLabelledBy="addressFormCategoriesLabel"
dropdownVendorCategories={dropdownVendorCategories}
name={name}
+ onChange={onCategoryChange}
/>
diff --git a/src/common/constants/api.js b/src/common/constants/api.js
index e4d3413e..a9055baa 100644
--- a/src/common/constants/api.js
+++ b/src/common/constants/api.js
@@ -1,7 +1,10 @@
+export const AGREEMENTS_API = 'erm/sas';
+export const BANKING_ACCOUNT_TYPES_API = 'organizations-storage/banking-account-types';
+export const BANKING_INFORMATION_API = 'organizations/banking-information';
export const CATEGORIES_API = 'organizations-storage/categories';
export const CONTACTS_API = 'organizations-storage/contacts';
export const INTERFACES_API = 'organizations-storage/interfaces';
+export const SETTINGS_API = 'organizations-storage/settings';
export const TYPES_API = 'organizations-storage/organization-types';
-export const AGREEMENTS_API = 'erm/sas';
export const MAX_LIMIT = 2147483647;
diff --git a/src/common/constants/events.js b/src/common/constants/events.js
new file mode 100644
index 00000000..78bbb388
--- /dev/null
+++ b/src/common/constants/events.js
@@ -0,0 +1,3 @@
+export const EVENT_EMITTER_EVENTS = {
+ ADDRESS_CATEGORY_CHANGED: 'ADDRESS_CATEGORY_CHANGED',
+};
diff --git a/src/common/constants/index.js b/src/common/constants/index.js
index 25a381e2..52453f15 100644
--- a/src/common/constants/index.js
+++ b/src/common/constants/index.js
@@ -1,5 +1,6 @@
export * from './api';
export * from './categories';
+export * from './events';
export * from './interfaces';
export * from './organization';
export * from './organizationTypes';
diff --git a/src/common/constants/organization.js b/src/common/constants/organization.js
index 97e7a7d5..d20894e1 100644
--- a/src/common/constants/organization.js
+++ b/src/common/constants/organization.js
@@ -1,3 +1,4 @@
export { ORGANIZATION_STATUS } from '@folio/plugin-find-organization';
+export const BANKING_INFORMATION_ENABLED_KEY = 'BANKING_INFORMATION_ENABLED';
export const DICT_CATEGORIES = 'vendorCategories';
diff --git a/src/common/hooks/index.js b/src/common/hooks/index.js
index 393e24a9..9b6c0fd4 100644
--- a/src/common/hooks/index.js
+++ b/src/common/hooks/index.js
@@ -1,6 +1,12 @@
export * from './useAcqMethods';
+export * from './useBankingAccountTypes';
+export * from './useBankingInformationMutation';
+export * from './useBankingInformationSettings';
+export * from './useCategories';
+export * from './useEventEmitter';
export * from './useIntegrationConfig';
export * from './useIntegrationConfigMutation';
export * from './useLinkedAgreements';
+export * from './useOrganizationBankingInformation';
export * from './useTranslatedCategories';
export * from './useTypes';
diff --git a/src/common/hooks/useBankingAccountTypes/index.js b/src/common/hooks/useBankingAccountTypes/index.js
new file mode 100644
index 00000000..c1574cdd
--- /dev/null
+++ b/src/common/hooks/useBankingAccountTypes/index.js
@@ -0,0 +1 @@
+export { useBankingAccountTypes } from './useBankingAccountTypes';
diff --git a/src/common/hooks/useBankingAccountTypes/useBankingAccountTypes.js b/src/common/hooks/useBankingAccountTypes/useBankingAccountTypes.js
new file mode 100644
index 00000000..aec118d8
--- /dev/null
+++ b/src/common/hooks/useBankingAccountTypes/useBankingAccountTypes.js
@@ -0,0 +1,40 @@
+import { useQuery } from 'react-query';
+
+import {
+ useNamespace,
+ useOkapiKy,
+} from '@folio/stripes/core';
+import { LIMIT_MAX } from '@folio/stripes-acq-components';
+
+import { BANKING_ACCOUNT_TYPES_API } from '../../constants';
+
+const DEFAULT_DATA = [];
+
+export const useBankingAccountTypes = () => {
+ const ky = useOkapiKy();
+ const [namespace] = useNamespace({ key: 'banking-account-types' });
+
+ const {
+ data,
+ isFetching,
+ isLoading,
+ refetch,
+ } = useQuery(
+ [namespace],
+ () => {
+ const searchParams = {
+ limit: LIMIT_MAX,
+ };
+
+ return ky.get(BANKING_ACCOUNT_TYPES_API, { searchParams }).json();
+ },
+ );
+
+ return ({
+ bankingAccountTypes: data?.bankingAccountTypes || DEFAULT_DATA,
+ totalRecords: data?.totalRecords,
+ isFetching,
+ isLoading,
+ refetch,
+ });
+};
diff --git a/src/common/hooks/useBankingAccountTypes/useBankingAccountTypes.test.js b/src/common/hooks/useBankingAccountTypes/useBankingAccountTypes.test.js
new file mode 100644
index 00000000..cb7eb2c6
--- /dev/null
+++ b/src/common/hooks/useBankingAccountTypes/useBankingAccountTypes.test.js
@@ -0,0 +1,37 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
+
+import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { useBankingAccountTypes } from './useBankingAccountTypes';
+
+const bankingAccountTypes = [{ id: 'banking-account-type-id' }];
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const kyMock = {
+ get: jest.fn(() => ({
+ json: () => Promise.resolve({ bankingAccountTypes }),
+ })),
+};
+
+describe('useBankingAccountTypes', () => {
+ beforeEach(() => {
+ kyMock.get.mockClear();
+ useOkapiKy.mockClear().mockReturnValue(kyMock);
+ });
+
+ it('should fetch all banking account types', async () => {
+ const { result } = renderHook(() => useBankingAccountTypes(), { wrapper });
+
+ await waitFor(() => expect(result.current.isLoading).toBeFalsy());
+
+ expect(result.current.bankingAccountTypes).toEqual(bankingAccountTypes);
+ });
+});
diff --git a/src/common/hooks/useBankingInformationMutation/index.js b/src/common/hooks/useBankingInformationMutation/index.js
new file mode 100644
index 00000000..5700e6a6
--- /dev/null
+++ b/src/common/hooks/useBankingInformationMutation/index.js
@@ -0,0 +1 @@
+export { useBankingInformationMutation } from './useBankingInformationMutation';
diff --git a/src/common/hooks/useBankingInformationMutation/useBankingInformationMutation.js b/src/common/hooks/useBankingInformationMutation/useBankingInformationMutation.js
new file mode 100644
index 00000000..111e51fc
--- /dev/null
+++ b/src/common/hooks/useBankingInformationMutation/useBankingInformationMutation.js
@@ -0,0 +1,52 @@
+import { useMutation } from 'react-query';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { BANKING_INFORMATION_API } from '../../constants';
+
+export const useBankingInformationMutation = () => {
+ const ky = useOkapiKy();
+
+ const {
+ mutateAsync: createBankingInformation,
+ isLoading: isBankingInformationCreateLoading,
+ } = useMutation({
+ mutationFn: ({ bankingInformation }) => {
+ return ky.post(BANKING_INFORMATION_API, { json: bankingInformation }).json();
+ },
+ });
+
+ const {
+ mutateAsync: updateBankingInformation,
+ isLoading: isBankingInformationUpdateLoading,
+ } = useMutation({
+ mutationFn: ({ bankingInformation }) => {
+ return ky.put(
+ `${BANKING_INFORMATION_API}/${bankingInformation.id}`,
+ { json: bankingInformation },
+ ).json();
+ },
+ });
+
+ const {
+ mutateAsync: deleteBankingInformation,
+ isLoading: isBankingInformationDeleteLoading,
+ } = useMutation({
+ mutationFn: ({ bankingInformation }) => {
+ return ky.delete(`${BANKING_INFORMATION_API}/${bankingInformation.id}`).json();
+ },
+ });
+
+ const isLoading = (
+ isBankingInformationCreateLoading
+ || isBankingInformationDeleteLoading
+ || isBankingInformationUpdateLoading
+ );
+
+ return {
+ createBankingInformation,
+ updateBankingInformation,
+ deleteBankingInformation,
+ isLoading,
+ };
+};
diff --git a/src/common/hooks/useBankingInformationMutation/useBankingInformationMutation.test.js b/src/common/hooks/useBankingInformationMutation/useBankingInformationMutation.test.js
new file mode 100644
index 00000000..a135fed3
--- /dev/null
+++ b/src/common/hooks/useBankingInformationMutation/useBankingInformationMutation.test.js
@@ -0,0 +1,83 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { renderHook } from '@folio/jest-config-stripes/testing-library/react';
+
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { BANKING_INFORMATION_API } from '../../constants';
+import { useBankingInformationMutation } from './useBankingInformationMutation';
+
+const bankingInformationBase = {
+ isPrimary: true,
+ organizationId: 'organizationId',
+ bankName: 'Bank name',
+};
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const buildMockRequest = (response) => () => ({
+ json: () => Promise.resolve(response),
+});
+
+const kyMock = {
+ post: jest.fn((_, { json }) => buildMockRequest(json)()),
+ put: jest.fn(buildMockRequest()),
+ delete: jest.fn(buildMockRequest()),
+};
+
+const bankingInformationId = 'banking-information-id';
+
+describe('useBankingInformationMutation', () => {
+ beforeEach(() => {
+ Object
+ .values(kyMock)
+ .forEach(fn => fn.mockClear());
+ useOkapiKy
+ .mockClear()
+ .mockReturnValue(kyMock);
+ });
+
+ it('should make POST request to create a new banking information', async () => {
+ const { result } = renderHook(
+ () => useBankingInformationMutation(),
+ { wrapper },
+ );
+
+ await result.current.createBankingInformation({ bankingInformation: bankingInformationBase });
+
+ expect(kyMock.post).toHaveBeenCalledWith(BANKING_INFORMATION_API, { json: bankingInformationBase });
+ });
+
+ it('should make PUT request to update an banking information', async () => {
+ const bankingInformation = {
+ ...bankingInformationBase,
+ id: bankingInformationId,
+ bankName: 'New bank name',
+ };
+
+ const { result } = renderHook(
+ () => useBankingInformationMutation(),
+ { wrapper },
+ );
+
+ await result.current.updateBankingInformation({ bankingInformation });
+
+ expect(kyMock.put).toHaveBeenCalledWith(`${BANKING_INFORMATION_API}/${bankingInformationId}`, { json: bankingInformation });
+ });
+
+ it('should make DELETE request to delete banking information by id', async () => {
+ const { result } = renderHook(
+ () => useBankingInformationMutation(),
+ { wrapper },
+ );
+
+ await result.current.deleteBankingInformation({ bankingInformation: { id: bankingInformationId } });
+
+ expect(kyMock.delete).toHaveBeenCalledWith(`${BANKING_INFORMATION_API}/${bankingInformationId}`);
+ });
+});
diff --git a/src/common/hooks/useBankingInformationSettings/index.js b/src/common/hooks/useBankingInformationSettings/index.js
new file mode 100644
index 00000000..b63dfa7c
--- /dev/null
+++ b/src/common/hooks/useBankingInformationSettings/index.js
@@ -0,0 +1 @@
+export { useBankingInformationSettings } from './useBankingInformationSettings';
diff --git a/src/common/hooks/useBankingInformationSettings/useBankingInformationSettings.js b/src/common/hooks/useBankingInformationSettings/useBankingInformationSettings.js
new file mode 100644
index 00000000..ab11faa6
--- /dev/null
+++ b/src/common/hooks/useBankingInformationSettings/useBankingInformationSettings.js
@@ -0,0 +1,46 @@
+import { get } from 'lodash';
+import { useQuery } from 'react-query';
+
+import {
+ useNamespace,
+ useOkapiKy,
+} from '@folio/stripes/core';
+
+import {
+ BANKING_INFORMATION_ENABLED_KEY,
+ SETTINGS_API,
+} from '../../constants';
+
+const DEFAULT_DATA = {};
+
+export const useBankingInformationSettings = () => {
+ const ky = useOkapiKy();
+ const [namespace] = useNamespace({ key: 'banking-information-settings' });
+
+ const {
+ data: bankingInformation = DEFAULT_DATA,
+ isFetching,
+ isLoading,
+ refetch,
+ } = useQuery(
+ [namespace],
+ async () => {
+ const response = await ky.get(SETTINGS_API, {
+ searchParams: {
+ query: `key=${BANKING_INFORMATION_ENABLED_KEY}`,
+ limit: 1,
+ },
+ }).json();
+
+ return get(response, 'settings[0]', DEFAULT_DATA);
+ },
+ );
+
+ return ({
+ bankingInformation,
+ enabled: bankingInformation.value === 'true',
+ isFetching,
+ isLoading,
+ refetch,
+ });
+};
diff --git a/src/Settings/hooks/useBankingInformation.test.js b/src/common/hooks/useBankingInformationSettings/useBankingInformationSettings.test.js
similarity index 62%
rename from src/Settings/hooks/useBankingInformation.test.js
rename to src/common/hooks/useBankingInformationSettings/useBankingInformationSettings.test.js
index 03046dfe..482b85cf 100644
--- a/src/Settings/hooks/useBankingInformation.test.js
+++ b/src/common/hooks/useBankingInformationSettings/useBankingInformationSettings.test.js
@@ -9,25 +9,25 @@ import {
} from '@folio/jest-config-stripes/testing-library/react';
import { useOkapiKy } from '@folio/stripes/core';
-import { useBankingInformation } from './useBankingInformation';
+import { BANKING_INFORMATION_ENABLED_KEY } from '../../constants';
+import { useBankingInformationSettings } from './useBankingInformationSettings';
const queryClient = new QueryClient();
const MOCK_BANKING_INFORMATION = {
- 'id': 'cb007def-4b68-496c-ad78-ea8e039e819d',
- 'key': 'BANKING_INFORMATION_ENABLED',
- 'value': 'true',
+ id: 'cb007def-4b68-496c-ad78-ea8e039e819d',
+ key: BANKING_INFORMATION_ENABLED_KEY,
+ value: 'true',
refetch: jest.fn(),
};
-// eslint-disable-next-line react/prop-types
const wrapper = ({ children }) => (
{children}
);
-describe('useBankingInformation', () => {
+describe('useBankingInformationSettings', () => {
beforeEach(() => {
useOkapiKy
.mockClear()
@@ -38,16 +38,15 @@ describe('useBankingInformation', () => {
});
});
- it('should fetch all organization types', async () => {
- const { result } = renderHook(() => useBankingInformation(), { wrapper });
+ it('should fetch banking information settings', async () => {
+ const { result } = renderHook(() => useBankingInformationSettings(), { wrapper });
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
expect(result.current).toEqual(expect.objectContaining({
enabled: true,
isLoading: false,
- id: MOCK_BANKING_INFORMATION.id,
- key: MOCK_BANKING_INFORMATION.key,
+ bankingInformation: MOCK_BANKING_INFORMATION,
}));
});
});
diff --git a/src/common/hooks/useCategories/index.js b/src/common/hooks/useCategories/index.js
new file mode 100644
index 00000000..8fa49339
--- /dev/null
+++ b/src/common/hooks/useCategories/index.js
@@ -0,0 +1 @@
+export { useCategories } from './useCategories';
diff --git a/src/common/hooks/useCategories/useCategories.js b/src/common/hooks/useCategories/useCategories.js
new file mode 100644
index 00000000..8cd3878b
--- /dev/null
+++ b/src/common/hooks/useCategories/useCategories.js
@@ -0,0 +1,41 @@
+import { useQuery } from 'react-query';
+
+import {
+ useNamespace,
+ useOkapiKy,
+} from '@folio/stripes/core';
+import { LIMIT_MAX } from '@folio/stripes-acq-components';
+
+import { CATEGORIES_API } from '../../constants';
+import { useTranslatedCategories } from '../useTranslatedCategories';
+
+const DEFAULT_DATA = [];
+
+export const useCategories = (options = {}) => {
+ const ky = useOkapiKy();
+ const [namespace] = useNamespace('categories');
+
+ const searchParams = {
+ limit: LIMIT_MAX,
+ query: 'cql.allRecords=1',
+ };
+
+ const {
+ data,
+ isFetching,
+ isLoading,
+ } = useQuery(
+ [namespace],
+ () => ky.get(CATEGORIES_API, { searchParams }).json(),
+ options,
+ );
+
+ const [translatedCategories] = useTranslatedCategories(data?.categories);
+
+ return ({
+ categories: translatedCategories || DEFAULT_DATA,
+ totalRecords: data?.totalRecords,
+ isFetching,
+ isLoading,
+ });
+};
diff --git a/src/common/hooks/useCategories/useCategories.test.js b/src/common/hooks/useCategories/useCategories.test.js
new file mode 100644
index 00000000..530ce269
--- /dev/null
+++ b/src/common/hooks/useCategories/useCategories.test.js
@@ -0,0 +1,37 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
+
+import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { useCategories } from './useCategories';
+
+const categories = [{ id: 'categoryId' }];
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const kyMock = {
+ get: jest.fn(() => ({
+ json: () => Promise.resolve({ categories }),
+ })),
+};
+
+describe('useCategories', () => {
+ beforeEach(() => {
+ kyMock.get.mockClear();
+ useOkapiKy.mockClear().mockReturnValue(kyMock);
+ });
+
+ it('should fetch all categories', async () => {
+ const { result } = renderHook(() => useCategories(), { wrapper });
+
+ await waitFor(() => expect(result.current.isLoading).toBeFalsy());
+
+ expect(result.current.categories).toEqual(categories);
+ });
+});
diff --git a/src/common/hooks/useEventEmitter/index.js b/src/common/hooks/useEventEmitter/index.js
new file mode 100644
index 00000000..2e7b4b78
--- /dev/null
+++ b/src/common/hooks/useEventEmitter/index.js
@@ -0,0 +1 @@
+export { useEventEmitter } from './useEventEmitter';
diff --git a/src/common/hooks/useEventEmitter/useEventEmitter.js b/src/common/hooks/useEventEmitter/useEventEmitter.js
new file mode 100644
index 00000000..6c699778
--- /dev/null
+++ b/src/common/hooks/useEventEmitter/useEventEmitter.js
@@ -0,0 +1,7 @@
+import { EventEmitter } from '../../utils';
+
+const eventEmitter = new EventEmitter();
+
+export const useEventEmitter = () => {
+ return eventEmitter;
+};
diff --git a/src/common/hooks/useEventEmitter/useEventEmitter.test.js b/src/common/hooks/useEventEmitter/useEventEmitter.test.js
new file mode 100644
index 00000000..475b9de6
--- /dev/null
+++ b/src/common/hooks/useEventEmitter/useEventEmitter.test.js
@@ -0,0 +1,12 @@
+import { renderHook } from '@folio/jest-config-stripes/testing-library/react';
+
+import { EventEmitter } from '../../utils';
+import { useEventEmitter } from './useEventEmitter';
+
+describe('useEventEmitter', () => {
+ it('should return event emitter instance', async () => {
+ const { result } = renderHook(() => useEventEmitter());
+
+ expect(result.current).toBeInstanceOf(EventEmitter);
+ });
+});
diff --git a/src/common/hooks/useOrganizationBankingInformation/index.js b/src/common/hooks/useOrganizationBankingInformation/index.js
new file mode 100644
index 00000000..b97f1380
--- /dev/null
+++ b/src/common/hooks/useOrganizationBankingInformation/index.js
@@ -0,0 +1 @@
+export { useOrganizationBankingInformation } from './useOrganizationBankingInformation';
diff --git a/src/common/hooks/useOrganizationBankingInformation/useOrganizationBankingInformation.js b/src/common/hooks/useOrganizationBankingInformation/useOrganizationBankingInformation.js
new file mode 100644
index 00000000..132f90c8
--- /dev/null
+++ b/src/common/hooks/useOrganizationBankingInformation/useOrganizationBankingInformation.js
@@ -0,0 +1,47 @@
+import { useQuery } from 'react-query';
+
+import {
+ useNamespace,
+ useOkapiKy,
+} from '@folio/stripes/core';
+import { LIMIT_MAX } from '@folio/stripes-acq-components';
+
+import { BANKING_INFORMATION_API } from '../../constants';
+
+const DEFAULT_DATA = [];
+
+export const useOrganizationBankingInformation = (organizationId, options = {}) => {
+ const ky = useOkapiKy();
+ const [namespace] = useNamespace({ key: 'organization-banking-information' });
+
+ const queryOptions = {
+ ...options,
+ enabled: options.enabled && Boolean(organizationId),
+ };
+
+ const {
+ data,
+ isFetching,
+ isLoading,
+ refetch,
+ } = useQuery(
+ [namespace],
+ () => {
+ const searchParams = {
+ query: `organizationId==${organizationId} sortby metadata.createdDate/sort.ascending`,
+ limit: LIMIT_MAX,
+ };
+
+ return ky.get(BANKING_INFORMATION_API, { searchParams }).json();
+ },
+ queryOptions,
+ );
+
+ return ({
+ bankingInformation: data?.bankingInformation || DEFAULT_DATA,
+ totalRecords: data?.totalRecords,
+ isFetching,
+ isLoading,
+ refetch,
+ });
+};
diff --git a/src/common/hooks/useOrganizationBankingInformation/useOrganizationBankingInformation.test.js b/src/common/hooks/useOrganizationBankingInformation/useOrganizationBankingInformation.test.js
new file mode 100644
index 00000000..43e629e8
--- /dev/null
+++ b/src/common/hooks/useOrganizationBankingInformation/useOrganizationBankingInformation.test.js
@@ -0,0 +1,43 @@
+import { QueryClient, QueryClientProvider } from 'react-query';
+
+import { renderHook, waitFor } from '@folio/jest-config-stripes/testing-library/react';
+import { useOkapiKy } from '@folio/stripes/core';
+
+import { organization } from 'fixtures';
+import { useOrganizationBankingInformation } from './useOrganizationBankingInformation';
+
+const queryClient = new QueryClient();
+
+const wrapper = ({ children }) => (
+
+ {children}
+
+);
+
+const organizationId = organization.id;
+const bankingInformation = [
+ { id: 'id', organizationId },
+];
+
+const kyMock = {
+ get: jest.fn(() => ({
+ json: () => Promise.resolve({ bankingInformation }),
+ })),
+};
+
+describe('useOrganizationBankingInformation', () => {
+ beforeEach(() => {
+ kyMock.get.mockClear();
+ useOkapiKy
+ .mockClear()
+ .mockReturnValue(kyMock);
+ });
+
+ it('should fetch organization banking information', async () => {
+ const { result } = renderHook(() => useOrganizationBankingInformation(organizationId), { wrapper });
+
+ await waitFor(() => expect(result.current.isLoading).toBeFalsy());
+
+ expect(result.current.bankingInformation).toEqual(bankingInformation);
+ });
+});
diff --git a/src/common/utils/EventEmitter/EventEmitter.js b/src/common/utils/EventEmitter/EventEmitter.js
new file mode 100644
index 00000000..3cffc881
--- /dev/null
+++ b/src/common/utils/EventEmitter/EventEmitter.js
@@ -0,0 +1,19 @@
+export class EventEmitter {
+ constructor() {
+ this.eventTarget = new EventTarget();
+ }
+
+ on(eventName, callback) {
+ this.eventTarget.addEventListener(eventName, callback);
+ }
+
+ off(eventName, callback) {
+ this.eventTarget.removeEventListener(eventName, callback);
+ }
+
+ emit(eventName, data) {
+ const event = new CustomEvent(eventName, { detail: data });
+
+ this.eventTarget.dispatchEvent(event);
+ }
+}
diff --git a/src/common/utils/EventEmitter/EventEmitter.test.js b/src/common/utils/EventEmitter/EventEmitter.test.js
new file mode 100644
index 00000000..ffcd0b9f
--- /dev/null
+++ b/src/common/utils/EventEmitter/EventEmitter.test.js
@@ -0,0 +1,36 @@
+import { EventEmitter } from './EventEmitter';
+
+const EVENT_TYPE = 'test-event-type';
+const callback = jest.fn();
+const payload = 'Test payload';
+
+describe('EventEmitter', () => {
+ let emitter;
+
+ beforeEach(() => {
+ emitter = new EventEmitter();
+ callback.mockClear();
+ });
+
+ it('should add and invoke event listeners', () => {
+ emitter.on(EVENT_TYPE, callback);
+ emitter.emit(EVENT_TYPE, payload);
+
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ detail: payload }));
+ });
+
+ it('should remove event listeners', () => {
+ emitter.on(EVENT_TYPE, callback);
+ emitter.off(EVENT_TYPE, callback);
+ emitter.emit(EVENT_TYPE, payload);
+
+ expect(callback).not.toHaveBeenCalled();
+ });
+
+ it('should emit events with the correct data', () => {
+ emitter.on(EVENT_TYPE, callback);
+ emitter.emit(EVENT_TYPE, payload);
+
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ detail: payload }));
+ });
+});
diff --git a/src/common/utils/EventEmitter/index.js b/src/common/utils/EventEmitter/index.js
new file mode 100644
index 00000000..4f497da0
--- /dev/null
+++ b/src/common/utils/EventEmitter/index.js
@@ -0,0 +1 @@
+export { EventEmitter } from './EventEmitter';
diff --git a/src/common/utils/createAddNewItem.js b/src/common/utils/createAddNewItem.js
index 9431000b..2b59ec9b 100644
--- a/src/common/utils/createAddNewItem.js
+++ b/src/common/utils/createAddNewItem.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/prefer-default-export
export function createAddNewItem(defaultLanguage) {
return fields => {
const newItem = {};
diff --git a/src/common/utils/getArrayItemsChanges/getArrayItemsChanges.js b/src/common/utils/getArrayItemsChanges/getArrayItemsChanges.js
new file mode 100644
index 00000000..78c0828d
--- /dev/null
+++ b/src/common/utils/getArrayItemsChanges/getArrayItemsChanges.js
@@ -0,0 +1,39 @@
+import isEqual from 'lodash/isEqual';
+import keyBy from 'lodash/keyBy';
+
+/**
+ * Detects added, modified and deleted items in an array.
+ * The function assumes that each element of the array is an object structure.
+*/
+export const getArrayItemsChanges = (initialValues = [], values = []) => {
+ const initialValuesMap = keyBy(initialValues, 'id');
+ const valuesMap = keyBy(values, 'id');
+
+ const { created, updated } = values.reduce((acc, item) => {
+ const initItem = initialValuesMap[item.id];
+
+ if (!initItem) {
+ acc.created.push(item);
+ } else if (!isEqual(initItem, item)) {
+ acc.updated.push(item);
+ }
+
+ return acc;
+ }, { created: [], updated: [] });
+
+ const deleted = initialValues.reduce((acc, initItem) => {
+ const item = valuesMap[initItem.id];
+
+ if (!item) {
+ acc.push(initItem);
+ }
+
+ return acc;
+ }, []);
+
+ return {
+ created,
+ updated,
+ deleted,
+ };
+};
diff --git a/src/common/utils/getArrayItemsChanges/getArrayItemsChanges.test.js b/src/common/utils/getArrayItemsChanges/getArrayItemsChanges.test.js
new file mode 100644
index 00000000..3da99314
--- /dev/null
+++ b/src/common/utils/getArrayItemsChanges/getArrayItemsChanges.test.js
@@ -0,0 +1,122 @@
+import cloneDeep from 'lodash/cloneDeep';
+
+import { getArrayItemsChanges } from './getArrayItemsChanges';
+
+const initialArray = [
+ { id: 1, foo: 'a' },
+ { id: 2, foo: 'b' },
+ { id: 3, foo: 'c' },
+ { id: 4, foo: 'd' },
+ { id: 5, foo: 'e' },
+];
+
+describe('getArrayItemsChanges', () => {
+ it('should return a list of created (added) items', () => {
+ const newItems = [
+ { id: 6, foo: 'f' },
+ { id: 7, foo: 'g' },
+ { id: 8, foo: 'e' },
+ ];
+ const currArray = [
+ ...initialArray,
+ ...newItems,
+ ];
+
+ const {
+ created,
+ deleted,
+ updated,
+ } = getArrayItemsChanges(initialArray, currArray);
+
+ expect(created).toHaveLength(3);
+ expect(created).toEqual(newItems);
+ expect(deleted).toHaveLength(0);
+ expect(updated).toHaveLength(0);
+ });
+
+ it('should return a list of updated items', () => {
+ const updatedItems = [
+ { id: 2, foo: 'bb' },
+ { id: 3, foo: 'cc' },
+ ];
+
+ const currArray = cloneDeep(initialArray).map(el => {
+ const updatedItem = updatedItems.find(item => item.id === el.id);
+
+ return updatedItem || el;
+ });
+
+ const {
+ created,
+ deleted,
+ updated,
+ } = getArrayItemsChanges(initialArray, currArray);
+
+ expect(created).toHaveLength(0);
+ expect(deleted).toHaveLength(0);
+ expect(updated).toHaveLength(2);
+ expect(updated).toEqual(updatedItems);
+ });
+
+ it('should return a list of deleted items', () => {
+ const deletedItems = initialArray.slice(0, 3);
+
+ const currArray = cloneDeep(initialArray).filter(el => {
+ const deletedItem = deletedItems.find(item => item.id === el.id);
+
+ return !deletedItem;
+ });
+
+ const {
+ created,
+ deleted,
+ updated,
+ } = getArrayItemsChanges(initialArray, currArray);
+
+ expect(created).toHaveLength(0);
+ expect(deleted).toHaveLength(3);
+ expect(deleted).toEqual(deletedItems);
+ expect(updated).toHaveLength(0);
+ });
+
+ it('should return a list with all changes in the array made at once', () => {
+ const newItems = [
+ { id: 6, foo: 'f' },
+ { id: 7, foo: 'g' },
+ ];
+ const updatedItems = [
+ { id: 1, foo: 'aa' },
+ { id: 2, foo: 'bb' },
+ { id: 3, foo: 'cc' },
+ ];
+ const deletedItems = initialArray.slice(3, 4);
+
+ const currArray = [
+ ...cloneDeep(initialArray),
+ ...newItems,
+ ]
+ .map(el => {
+ const updatedItem = updatedItems.find(item => item.id === el.id);
+
+ return updatedItem || el;
+ })
+ .filter(el => {
+ const deletedItem = deletedItems.find(item => item.id === el.id);
+
+ return !deletedItem;
+ });
+
+ const {
+ created,
+ deleted,
+ updated,
+ } = getArrayItemsChanges(initialArray, currArray);
+
+ expect(created).toHaveLength(2);
+ expect(created).toEqual(newItems);
+ expect(deleted).toHaveLength(1);
+ expect(deleted).toEqual(deletedItems);
+ expect(updated).toHaveLength(3);
+ expect(updated).toEqual(updatedItems);
+ });
+});
diff --git a/src/common/utils/getArrayItemsChanges/index.js b/src/common/utils/getArrayItemsChanges/index.js
new file mode 100644
index 00000000..c4deb405
--- /dev/null
+++ b/src/common/utils/getArrayItemsChanges/index.js
@@ -0,0 +1 @@
+export { getArrayItemsChanges } from './getArrayItemsChanges';
diff --git a/src/common/utils/index.js b/src/common/utils/index.js
index 5db960aa..8e662bd0 100644
--- a/src/common/utils/index.js
+++ b/src/common/utils/index.js
@@ -1,5 +1,7 @@
export * from './category';
export * from './createItem';
+export * from './EventEmitter';
+export * from './getArrayItemsChanges';
export * from './getResourceData';
export * from './hydrateContactInfo';
export * from './createAddNewItem';
diff --git a/src/contacts/EditContact/__snapshots__/EditContact.test.js.snap b/src/contacts/EditContact/__snapshots__/EditContact.test.js.snap
index 8952df91..57812565 100644
--- a/src/contacts/EditContact/__snapshots__/EditContact.test.js.snap
+++ b/src/contacts/EditContact/__snapshots__/EditContact.test.js.snap
@@ -31869,7 +31869,7 @@ exports[`EditContact should render correct form structure 1`] = `
class="optionSegment"
data-test-selection-option-segment="true"
>
- stripes-components.countries.US
+ US
@@ -31902,7 +31902,7 @@ exports[`EditContact should render correct form structure 1`] = `
class="selectionFilterContainer"
>
- stripes-components.countries.AF
+ AD
- stripes-components.countries.AX
+ AE
- stripes-components.countries.AL
+ AF
- stripes-components.countries.DZ
+ AG
- stripes-components.countries.AS
+ AI
- stripes-components.countries.AD
+ AL
- stripes-components.countries.AO
+ AM
- stripes-components.countries.AI
+ AN
- stripes-components.countries.AQ
+ AO
- stripes-components.countries.AG
+ AQ
- stripes-components.countries.AR
+ AR
- stripes-components.countries.AM
+ AS
- stripes-components.countries.AW
+ AT
- stripes-components.countries.AU
+ AU
- stripes-components.countries.AT
+ AW
- stripes-components.countries.AZ
+ AX
- stripes-components.countries.BS
+ AZ
- stripes-components.countries.BH
+ BA
- stripes-components.countries.BD
+ BB
- stripes-components.countries.BB
+ BD
- stripes-components.countries.BY
+ BE
- stripes-components.countries.BE
+ BF
- stripes-components.countries.BZ
+ BG
- stripes-components.countries.BJ
+ BH
- stripes-components.countries.BM
+ BI
- stripes-components.countries.BT
+ BJ
- stripes-components.countries.BO
+ BL
- stripes-components.countries.BA
+ BM
- stripes-components.countries.BW
+ BN
- stripes-components.countries.BV
+ BO
- stripes-components.countries.BR
+ BR
- stripes-components.countries.VG
+ BS
- stripes-components.countries.IO
+ BT
- stripes-components.countries.BN
+ BV
- stripes-components.countries.BG
+ BW
- stripes-components.countries.BF
+ BY
- stripes-components.countries.BI
+ BZ
- stripes-components.countries.KH
+ CA
- stripes-components.countries.CM
+ CC
- stripes-components.countries.CA
+ CD
- stripes-components.countries.CV
+ CF
- stripes-components.countries.KY
+ CG
- stripes-components.countries.CF
+ CH
- stripes-components.countries.TD
+ CI
- stripes-components.countries.CL
+ CK
- stripes-components.countries.CN
+ CL
- stripes-components.countries.HK
+ CM
- stripes-components.countries.MO
+ CN
- stripes-components.countries.CX
+ CO
- stripes-components.countries.CC
+ CR
- stripes-components.countries.CO
+ CU
- stripes-components.countries.KM
+ CV
- stripes-components.countries.CG
+ CX
- stripes-components.countries.CD
+ CY
- stripes-components.countries.CK
+ CZ
- stripes-components.countries.CR
+ DE
- stripes-components.countries.CI
+ DJ
- stripes-components.countries.HR
+ DK
- stripes-components.countries.CU
+ DM
- stripes-components.countries.CY
+ DO
- stripes-components.countries.CZ
+ DZ
- stripes-components.countries.DK
+ EC
- stripes-components.countries.DJ
+ EE
- stripes-components.countries.DM
+ EG
- stripes-components.countries.DO
+ EH
- stripes-components.countries.EC
+ ER
- stripes-components.countries.EG
+ ES
- stripes-components.countries.SV
+ ET
- stripes-components.countries.GQ
+ FI
- stripes-components.countries.ER
+ FJ
- stripes-components.countries.EE
+ FK
- stripes-components.countries.ET
+ FM
- stripes-components.countries.FK
+ FO
- stripes-components.countries.FO
+ FR
- stripes-components.countries.FJ
+ GA
- stripes-components.countries.FI
+ GB
- stripes-components.countries.FR
+ GD
- stripes-components.countries.GF
+ GE
- stripes-components.countries.PF
+ GF
- stripes-components.countries.TF
+ GG
- stripes-components.countries.GA
+ GH
- stripes-components.countries.GM
+ GI
- stripes-components.countries.GE
+ GL
- stripes-components.countries.DE
+ GM
- stripes-components.countries.GH
+ GN
- stripes-components.countries.GI
+ GP
- stripes-components.countries.GR
+ GQ
- stripes-components.countries.GL
+ GR
- stripes-components.countries.GD
+ GS
- stripes-components.countries.GP
+ GT
- stripes-components.countries.GU
+ GU
- stripes-components.countries.GT
+ GW
- stripes-components.countries.GG
+ GY
- stripes-components.countries.GN
+ HK
- stripes-components.countries.GW
+ HM
- stripes-components.countries.GY
+ HN
- stripes-components.countries.HT
+ HR
- stripes-components.countries.HM
+ HT
- stripes-components.countries.VA
+ HU
- stripes-components.countries.HN
+ ID
- stripes-components.countries.HU
+ IE
- stripes-components.countries.IS
+ IL
- stripes-components.countries.IN
+ IM
- stripes-components.countries.ID
+ IN
- stripes-components.countries.IR
+ IO
- stripes-components.countries.IQ
+ IQ
- stripes-components.countries.IE
+ IR
- stripes-components.countries.IM
+ IS
- stripes-components.countries.IL
+ IT
- stripes-components.countries.IT
+ JE
- stripes-components.countries.JM
+ JM
- stripes-components.countries.JP
+ JO
- stripes-components.countries.JE
+ JP
- stripes-components.countries.JO
+ KE
- stripes-components.countries.KZ
+ KG
- stripes-components.countries.KE
+ KH
- stripes-components.countries.KI
+ KI
- stripes-components.countries.KP
+ KM
- stripes-components.countries.KR
+ KN
- stripes-components.countries.KW
+ KP
- stripes-components.countries.KG
+ KR
- stripes-components.countries.LA
+ KW
- stripes-components.countries.LV
+ KY
- stripes-components.countries.LB
+ KZ
- stripes-components.countries.LS
+ LA
- stripes-components.countries.LR
+ LB
- stripes-components.countries.LY
+ LC
- stripes-components.countries.LI
+ LI
- stripes-components.countries.LT
+ LK
- stripes-components.countries.LU
+ LR
- stripes-components.countries.MK
+ LS
- stripes-components.countries.MG
+ LT
- stripes-components.countries.MW
+ LU
- stripes-components.countries.MY
+ LV
- stripes-components.countries.MV
+ LY
- stripes-components.countries.ML
+ MA
- stripes-components.countries.MT
+ MC
- stripes-components.countries.MH
+ MD
- stripes-components.countries.MQ
+ ME
- stripes-components.countries.MR
+ MF
- stripes-components.countries.MU
+ MG
- stripes-components.countries.YT
+ MH
- stripes-components.countries.MX
+ MK
- stripes-components.countries.FM
+ ML
- stripes-components.countries.MD
+ MM
- stripes-components.countries.MC
+ MN
- stripes-components.countries.MN
+ MO
- stripes-components.countries.ME
+ MP
- stripes-components.countries.MS
+ MQ
- stripes-components.countries.MA
+ MR
- stripes-components.countries.MZ
+ MS
- stripes-components.countries.MM
+ MT
- stripes-components.countries.NA
+ MU
- stripes-components.countries.NR
+ MV
- stripes-components.countries.NP
+ MW
- stripes-components.countries.NL
+ MX
- stripes-components.countries.AN
+ MY
- stripes-components.countries.NC
+ MZ
- stripes-components.countries.NZ
+ NA
- stripes-components.countries.NI
+ NC
- stripes-components.countries.NE
+ NE
- stripes-components.countries.NG
+ NF
- stripes-components.countries.NU
+ NG
- stripes-components.countries.NF
+ NI
- stripes-components.countries.MP
+ NL
- stripes-components.countries.NO
+ NO
- stripes-components.countries.OM
+ NP
- stripes-components.countries.PK
+ NR
- stripes-components.countries.PW
+ NU
- stripes-components.countries.PS
+ NZ
- stripes-components.countries.PA
+ OM
- stripes-components.countries.PG
+ PA
- stripes-components.countries.PY
+ PE
- stripes-components.countries.PE
+ PF
- stripes-components.countries.PH
+ PG
- stripes-components.countries.PN
+ PH
- stripes-components.countries.PL
+ PK
- stripes-components.countries.PT
+ PL
- stripes-components.countries.PR
+ PM
- stripes-components.countries.QA
+ PN
- stripes-components.countries.RE
+ PR
- stripes-components.countries.RO
+ PS
- stripes-components.countries.RU
+ PT
- stripes-components.countries.RW
+ PW
- stripes-components.countries.BL
+ PY
- stripes-components.countries.SH
+ QA
- stripes-components.countries.KN
+ RE
- stripes-components.countries.LC
+ RO
- stripes-components.countries.MF
+ RS
- stripes-components.countries.PM
+ RU
- stripes-components.countries.VC
+ RW
- stripes-components.countries.WS
+ SA
- stripes-components.countries.SM
+ SB
- stripes-components.countries.ST
+ SC
- stripes-components.countries.SA
+ SD
- stripes-components.countries.SN
+ SE
- stripes-components.countries.RS
+ SG
- stripes-components.countries.SC
+ SH
- stripes-components.countries.SL
+ SI
- stripes-components.countries.SG
+ SJ
- stripes-components.countries.SK
+ SK
- stripes-components.countries.SI
+ SL
- stripes-components.countries.SB
+ SM
- stripes-components.countries.SO
+ SN
- stripes-components.countries.ZA
+ SO
- stripes-components.countries.GS
+ SR
- stripes-components.countries.SS
+ SS
- stripes-components.countries.ES
+ ST
- stripes-components.countries.LK
+ SV
- stripes-components.countries.SD
+ SY
- stripes-components.countries.SR
+ SZ
- stripes-components.countries.SJ
+ TC
- stripes-components.countries.SZ
+ TD
- stripes-components.countries.SE
+ TF
- stripes-components.countries.CH
+ TG
- stripes-components.countries.SY
+ TH
- stripes-components.countries.TW
+ TJ
- stripes-components.countries.TJ
+ TK
- stripes-components.countries.TZ
+ TL
- stripes-components.countries.TH
+ TM
- stripes-components.countries.TL
+ TN
- stripes-components.countries.TG
+ TO
- stripes-components.countries.TK
+ TR
- stripes-components.countries.TO
+ TT
- stripes-components.countries.TT
+ TV
- stripes-components.countries.TN
+ TW
- stripes-components.countries.TR
+ TZ
- stripes-components.countries.TM
+ UA
- stripes-components.countries.TC
+ UG
- stripes-components.countries.TV
+ UM
- stripes-components.countries.UG
+ US
- stripes-components.countries.UA
+ UY
- stripes-components.countries.AE
+ UZ
- stripes-components.countries.GB
+ VA
- stripes-components.countries.US
+ VC
- stripes-components.countries.UM
+ VE
- stripes-components.countries.UY
+ VG
- stripes-components.countries.UZ
+ VI
- stripes-components.countries.VU
+ VN
- stripes-components.countries.VE
+ VU
- stripes-components.countries.VN
+ WF
- stripes-components.countries.VI
+ WS
- stripes-components.countries.WF
+ YE
- stripes-components.countries.EH
+ YT
- stripes-components.countries.YE
+ ZA
- stripes-components.countries.ZM
+ ZM
- stripes-components.countries.ZW
+ ZW
diff --git a/src/contacts/ViewContact/ViewContactContainer.test.js b/src/contacts/ViewContact/ViewContactContainer.test.js
index 17e93a41..618963e0 100644
--- a/src/contacts/ViewContact/ViewContactContainer.test.js
+++ b/src/contacts/ViewContact/ViewContactContainer.test.js
@@ -1,13 +1,14 @@
-import React from 'react';
import { render, screen } from '@folio/jest-config-stripes/testing-library/react';
import { match, history } from '../../../test/jest/routerMocks';
-
import { DICT_CATEGORIES } from '../../common/constants';
import ViewContact from './ViewContact';
import { ViewContactContainer } from './ViewContactContainer';
-jest.mock('../../common/utils', () => ({ getResourceDataItem: jest.fn() }));
+jest.mock('../../common/utils', () => ({
+ ...jest.requireActual('../../common/utils'),
+ getResourceDataItem: jest.fn(),
+}));
jest.mock('./ViewContact', () => jest.fn(() => 'ViewContact'));
const historyMock = {
diff --git a/translations/ui-organizations/en.json b/translations/ui-organizations/en.json
index ec12c26e..66596301 100644
--- a/translations/ui-organizations/en.json
+++ b/translations/ui-organizations/en.json
@@ -12,6 +12,13 @@
"comingSoon": "Coming in a future release.",
+ "data.bankingInformation.accountType": "Account type",
+ "data.bankingInformation.addressCategory": "Address category",
+ "data.bankingInformation.bankAccountNumber": "Bank account number",
+ "data.bankingInformation.bankName": "Bank name",
+ "data.bankingInformation.isPrimary": "Use as primary banking information",
+ "data.bankingInformation.notes": "Notes",
+ "data.bankingInformation.transitNumber": "Transit number",
"data.contactTypes.address": "Address",
"data.contactTypes.addressLine1": "Address 1",
"data.contactTypes.addressLine2": "Address 2",
@@ -84,6 +91,8 @@
"vendor.confirmation.message": "Warning. All vendor information, vendor terms, EDI information, and accounts will be deleted from this record.",
"summary": "Summary",
+ "bankingInformation": "Banking information",
+ "bankingInformation.save.error": "Failed to save banking information.",
"contactInformation": "Contact information",
"contactPeople": "Contact people",
"agreements": "Agreements",
@@ -120,6 +129,7 @@
"summary.add": "Add alternative names",
"summary.pleaseAddAltNames": "Please add alternative names",
+ "button.bankingInformation.add": "Add banking information",
"button.cancel": "Cancel",
"button.saveAndClose": "Save & close",