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 }) => ( +
children} + /> +); + +const renderBankingInformationField = (props = {}) => render( + , + { wrapper }, +); + +describe('BankingInformationField', () => { + it('should render banking information item fields', () => { + renderBankingInformationField(); + + expect(screen.getByText('ui-organizations.data.bankingInformation.isPrimary')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.data.bankingInformation.bankName')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.data.bankingInformation.bankAccountNumber')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.data.bankingInformation.transitNumber')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.data.bankingInformation.addressCategory')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.data.bankingInformation.accountType')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.data.bankingInformation.notes')).toBeInTheDocument(); + }); +}); diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/index.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/index.js new file mode 100644 index 00000000..70a06bd3 --- /dev/null +++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationField/index.js @@ -0,0 +1 @@ +export { BankingInformationField } from './BankingInformationField'; diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js new file mode 100644 index 00000000..641f9189 --- /dev/null +++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import { useEffect } from 'react'; +import { useForm } from 'react-final-form'; + +import { RepeatableFieldWithValidation } from '@folio/stripes-acq-components'; + +import { EVENT_EMITTER_EVENTS } from '../../../../common/constants'; +import { useEventEmitter } from '../../../../common/hooks'; +import { getAddressCategoryIdsSet } from '../../getAddressCategoryIdsSet'; + +export const BankingInformationFieldArray = (props) => { + const { fields } = props; + const eventEmitter = useEventEmitter(); + const { change, getFieldState } = useForm(); + + /* + Handles organization addresses categories change. + Resets banking information address category fields without initial value. + */ + useEffect(() => { + const eventType = EVENT_EMITTER_EVENTS.ADDRESS_CATEGORY_CHANGED; + const callback = () => { + const addressesCategoriesIdsMap = getAddressCategoryIdsSet(getFieldState('addresses').value); + + fields.forEach(field => { + const fieldName = `${field}.categoryId`; + const { initial, value } = getFieldState(fieldName); + + if (!addressesCategoriesIdsMap.has(value) && value !== initial) { + change(fieldName, undefined); + } + }); + }; + + eventEmitter.on(eventType, callback); + + return () => { + eventEmitter.off(eventType, callback); + }; + }, [change, eventEmitter, fields, getFieldState]); + + return ; +}; + +BankingInformationFieldArray.propTypes = { + fields: PropTypes.object.isRequired, +}; diff --git a/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.test.js b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.test.js new file mode 100644 index 00000000..d58854d0 --- /dev/null +++ b/src/Organizations/OrganizationForm/OrganizationBankingInfoForm/BankingInformationFieldArray/BankingInformationFieldArray.test.js @@ -0,0 +1,39 @@ +import { MemoryRouter } from 'react-router-dom'; + +import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; +import stripesFinalForm from '@folio/stripes/final-form'; + +import { BankingInformationFieldArray } from './BankingInformationFieldArray'; + +jest.mock('@folio/stripes-acq-components', () => ({ + ...jest.requireActual('@folio/stripes-acq-components'), + RepeatableFieldWithValidation: jest.fn(() => 'RepeatableFieldWithValidation'), +})); + +const FormWrapper = stripesFinalForm({ + keepDirtyOnReinitialize: true, + navigationCheck: true, + subscription: { values: true }, + validateOnBlur: true, +})(({ children }) => {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
{children}
; +}); + +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 }) => ( + +
    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",