diff --git a/CHANGELOG.md b/CHANGELOG.md index 07078bfc..c3326e45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 5.1.0 (IN PROGRESS) * Designate Organization as donor. Refs UIORGS-383. +* Settings for banking information. Refs UIORGS-391. ## [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 4d765cd7..158442d4 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "organizations-storage.emails": "1.1", "organizations-storage.interfaces": "2.1", "organizations-storage.phone-numbers": "2.0", + "organizations-storage.settings": "1.0", "organizations-storage.urls": "1.1", "tags": "1.0", "users": "15.1 16.0" @@ -207,7 +208,11 @@ "settings.organizations.enabled", "organizations-storage.organization-types.collection.get", "organizations-storage.organization-types.item.get", - "organizations-storage.categories.collection.get" + "organizations-storage.categories.collection.get", + "organizations-storage.settings.collection.get", + "organizations-storage.settings.item.get", + "organizations-storage.banking-account-types.collection.get", + "organizations-storage.banking-account-types.item.get" ] }, { @@ -221,7 +226,11 @@ "organizations-storage.organization-types.item.delete", "organizations-storage.categories.item.delete", "organizations-storage.categories.item.post", - "organizations-storage.categories.item.put" + "organizations-storage.categories.item.put", + "organizations-storage.settings.item.put", + "organizations-storage.banking-account-types.item.post", + "organizations-storage.banking-account-types.item.put", + "organizations-storage.banking-account-types.item.delete" ] } ] diff --git a/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js b/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js new file mode 100644 index 00000000..0ffb2568 --- /dev/null +++ b/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.js @@ -0,0 +1,60 @@ +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'; + +const setUniqValidation = (value, index, items) => { + const errors = {}; + + const isBankingAccountTypeExist = items.some(({ id, name }) => { + return name?.toLowerCase() === value?.name?.toLowerCase() && id !== value?.id; + }); + + if (isBankingAccountTypeExist) { + errors.name = ; + } + + return errors; +}; + +const BankingAccountTypeSettings = () => { + const stripes = useStripes(); + const ConnectedComponent = stripes.connect(ControlledVocab); + + const columnMapping = { + name: , + action: , + }; + + const hasEditPerms = stripes.hasPerm('ui-organizations.settings'); + const actionSuppressor = { + edit: () => !hasEditPerms, + delete: () => !hasEditPerms, + }; + + return ( + } + translations={getControlledVocabTranslations('ui-organizations.settings.bankingAccountTypes')} + objectLabel="BankingAccountTypes" + visibleFields={['name']} + columnMapping={columnMapping} + hiddenFields={['lastUpdated', 'numberOfObjects']} + nameKey="bankingAccountTypes" + id="bankingAccountTypes" + validate={setUniqValidation} + sortby="name" + /> + ); +}; + +export default BankingAccountTypeSettings; diff --git a/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.test.js b/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.test.js new file mode 100644 index 00000000..fec7b865 --- /dev/null +++ b/src/Settings/BankingAccountTypeSettings/BankingAccountTypeSettings.test.js @@ -0,0 +1,66 @@ +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import { useStripes } from '@folio/stripes/core'; +import { ControlledVocab } from '@folio/stripes/smart-components'; + +import BankingAccountTypeSettings from './BankingAccountTypeSettings'; + +jest.mock('@folio/stripes/core', () => ({ + ...jest.requireActual('@folio/stripes/core'), + useStripes: jest.fn(), +})); + +jest.mock('@folio/stripes-smart-components/lib/ControlledVocab', () => jest.fn(({ + rowFilter, + label, + rowFilterFunction, + preCreateHook, + listSuppressor, +}) => ( + <> + {label} +
{rowFilter}
+ + +))); + +const stripesMock = { + connect: component => component, + hasPerm: jest.fn(() => true), + clone: jest.fn(), +}; + +const renderCategorySettings = () => render(); + +describe('BankingAccountTypeSettings', () => { + beforeEach(() => { + useStripes.mockReturnValue(stripesMock); + }); + + it('should render component', () => { + renderCategorySettings(); + + expect(screen.getByText('New')); + expect(screen.getByText('ui-organizations.settings.bankingAccountTypes')); + }); + + it('should check action suppression', () => { + renderCategorySettings(); + + const { actionSuppressor } = ControlledVocab.mock.calls[0][0]; + + expect(actionSuppressor.edit()).toBeFalsy(); + expect(actionSuppressor.delete()).toBeFalsy(); + }); +}); diff --git a/src/Settings/BankingAccountTypeSettings/index.js b/src/Settings/BankingAccountTypeSettings/index.js new file mode 100644 index 00000000..52570c07 --- /dev/null +++ b/src/Settings/BankingAccountTypeSettings/index.js @@ -0,0 +1 @@ +export { default as BankingAccountTypeSettings } from './BankingAccountTypeSettings'; diff --git a/src/Settings/BankingInformationSettings/BankingInformationSettings.js b/src/Settings/BankingInformationSettings/BankingInformationSettings.js new file mode 100644 index 00000000..5c45c571 --- /dev/null +++ b/src/Settings/BankingInformationSettings/BankingInformationSettings.js @@ -0,0 +1,53 @@ +import { FormattedMessage } from 'react-intl'; + +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 BankingInformationSettingsForm from './BankingInformationSettingsForm'; + +const BankingInformationSettings = () => { + const { + enabled, + key, + id: bankingInformationId, + version, + isLoading, + refetch, + } = useBankingInformation(); + const ky = useOkapiKy(); + const sendCallout = useShowCallout(); + + const onSubmit = async ({ value }) => { + try { + await ky.put(`${SETTINGS_API}/${bankingInformationId}`, { + json: { value, key, _version: version }, + }); + + refetch(); + sendCallout({ + message: , + }); + } catch (error) { + sendCallout({ + type: 'error', + message: , + }); + } + }; + + if (isLoading) { + return ; + } + + return ( + + ); +}; + +export default BankingInformationSettings; diff --git a/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js b/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js new file mode 100644 index 00000000..4bacedac --- /dev/null +++ b/src/Settings/BankingInformationSettings/BankingInformationSettings.test.js @@ -0,0 +1,84 @@ +import { MemoryRouter } from 'react-router-dom'; + +import { + act, + 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 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, + })), +})); + +const renderBankingInformationSettings = () => render( + , + { wrapper: MemoryRouter }, +); + +describe('BankingInformationSettings component', () => { + it('should display pane headings', () => { + renderBankingInformationSettings(); + + const paneTitle = screen.getByText('ui-organizations.settings.bankingInformation'); + const checkboxLabel = screen.getByText('ui-organizations.settings.bankingInformation.enable'); + + expect(paneTitle).toBeInTheDocument(); + expect(checkboxLabel).toBeInTheDocument(); + }); + + it('should render Loading component', () => { + useBankingInformation.mockReturnValue({ + isLoading: true, + enabled: false, + }); + + renderBankingInformationSettings(); + + expect(screen.getByText('Loading')).toBeInTheDocument(); + }); + + it('should save banking options', async () => { + useBankingInformation.mockClear().mockReturnValue({ + isLoading: false, + enabled: true, + refetch: mockRefetch, + }); + const mockPutMethod = jest.fn(() => ({ + json: () => Promise.resolve('ok'), + })); + + useOkapiKy + .mockClear() + .mockReturnValue({ + put: mockPutMethod, + }); + + 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); + }); + + expect(mockPutMethod).toHaveBeenCalled(); + }); +}); diff --git a/src/Settings/BankingInformationSettings/BankingInformationSettingsForm.js b/src/Settings/BankingInformationSettings/BankingInformationSettingsForm.js new file mode 100644 index 00000000..96d21441 --- /dev/null +++ b/src/Settings/BankingInformationSettings/BankingInformationSettingsForm.js @@ -0,0 +1,73 @@ +import { useMemo } from 'react'; +import PropTypes from 'prop-types'; +import { Field } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; + +import { + Button, + Checkbox, + Col, + Pane, + PaneFooter, + PaneHeader, + Row, +} from '@folio/stripes/components'; +import stripesForm from '@folio/stripes/final-form'; + +const BankingInformationSettingsForm = ({ + handleSubmit, + pristine, + submitting, +}) => { + const paneFooter = useMemo(() => { + const end = ( + + ); + + return ; + }, [handleSubmit, pristine, submitting]); + + const paneTitle = ; + + return ( + } + footer={paneFooter} + > + + + } + name="value" + type="checkbox" + /> + + + + ); +}; + +BankingInformationSettingsForm.propTypes = { + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, + renderHeader: PropTypes.func, +}; + +export default stripesForm({ + enableReinitialize: true, + keepDirtyOnReinitialize: true, + navigationCheck: true, + subscription: { values: true }, +})(BankingInformationSettingsForm); diff --git a/src/Settings/BankingInformationSettings/BankingInformationSettingsForm.test.js b/src/Settings/BankingInformationSettings/BankingInformationSettingsForm.test.js new file mode 100644 index 00000000..30b76e28 --- /dev/null +++ b/src/Settings/BankingInformationSettings/BankingInformationSettingsForm.test.js @@ -0,0 +1,46 @@ +import { MemoryRouter } from 'react-router-dom'; + +import { + act, + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import user from '@folio/jest-config-stripes/testing-library/user-event'; + +import BankingInformationSettingsForm from './BankingInformationSettingsForm'; + +const mockOnSubmit = jest.fn(); + +const DEFAULT_PROPS = { + onSubmit: mockOnSubmit, + initialValues: { + value: true, + }, +}; + +const renderBankingInformationSettingsForm = (props = DEFAULT_PROPS) => render( + , + { wrapper: MemoryRouter }, +); + +describe('BankingInformationSettingsForm component', () => { + it('should render component', async () => { + renderBankingInformationSettingsForm(); + + expect(screen.getByLabelText('ui-organizations.settings.bankingInformation')).toBeInTheDocument(); + }); + + it('should save banking options', async () => { + renderBankingInformationSettingsForm(); + + 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); + }); + + expect(mockOnSubmit).toHaveBeenCalled(); + }); +}); diff --git a/src/Settings/BankingInformationSettings/index.js b/src/Settings/BankingInformationSettings/index.js new file mode 100644 index 00000000..6fe1ba6f --- /dev/null +++ b/src/Settings/BankingInformationSettings/index.js @@ -0,0 +1 @@ +export { default as BankingInformationSettings } from './BankingInformationSettings'; diff --git a/src/Settings/SettingsPage.js b/src/Settings/SettingsPage.js index 65d7c8d4..e422f927 100644 --- a/src/Settings/SettingsPage.js +++ b/src/Settings/SettingsPage.js @@ -1,10 +1,14 @@ -import React from 'react'; +import React, { 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 { CategorySettings } from './CategorySettings'; import { TypeSettings } from './TypeSettings'; +import { BankingInformationSettings } from './BankingInformationSettings'; +import { BankingAccountTypeSettings } from './BankingAccountTypeSettings'; const pages = [ { @@ -19,14 +23,38 @@ const pages = [ perm: 'settings.organizations.enabled', route: 'type', }, + { + component: BankingInformationSettings, + label: , + perm: 'settings.organizations.enabled', + route: 'banking-information', + }, ]; -const SettingsPage = (props) => ( - } - /> -); +const bankingAccountTypesPage = { + component: BankingAccountTypeSettings, + label: , + perm: 'settings.organizations.enabled', + route: 'banking-account-types', +}; + +const SettingsPage = (props) => { + const { enabled, isLoading } = useBankingInformation(); + + const settingsPages = useMemo(() => (enabled ? pages.concat(bankingAccountTypesPage) : pages), [enabled]); + + if (isLoading) { + return ; + } + + return ( + } + /> + ); +}; export default SettingsPage; diff --git a/src/Settings/SettingsPage.test.js b/src/Settings/SettingsPage.test.js new file mode 100644 index 00000000..d4b2e8a0 --- /dev/null +++ b/src/Settings/SettingsPage.test.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { screen, render } from '@folio/jest-config-stripes/testing-library/react'; + +import SettingsPage from './SettingsPage'; +import { useBankingInformation } from './hooks'; + +jest.mock('@folio/stripes/core'); +jest.mock('@folio/stripes/smart-components'); + +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), + Loading: () =>
Loading
, +})); +jest.mock('./hooks', () => ({ + useBankingInformation: jest.fn(() => ({ + isLoading: false, + enabled: false, + })), +})); + +const stripesMock = { + connect: component => component, + hasPerm: jest.fn(() => true), +}; + +const defaultProps = { + stripes: stripesMock, + match: { + path: 'url', + }, + location: { + search: '?name=test', + pathname: '', + }, +}; + +const renderSettingsPage = (props) => render( + + + , +); + +describe('SettingsPage', () => { + it('should return categories, types and banking information links', async () => { + renderSettingsPage(); + + expect(screen.getByText('ui-organizations.settings.categories')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.settings.types')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.settings.bankingInformation')).toBeInTheDocument(); + }); + + it('should return banking account types link', async () => { + useBankingInformation.mockReturnValue({ + isLoading: false, + enabled: true, + }); + + renderSettingsPage(); + + 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 new file mode 100644 index 00000000..56157dab --- /dev/null +++ b/src/Settings/constants.js @@ -0,0 +1,8 @@ +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 new file mode 100644 index 00000000..2144f705 --- /dev/null +++ b/src/Settings/hooks/index.js @@ -0,0 +1 @@ +export { useBankingInformation } from './useBankingInformation'; diff --git a/src/Settings/hooks/useBankingInformation.js b/src/Settings/hooks/useBankingInformation.js new file mode 100644 index 00000000..23dce38d --- /dev/null +++ b/src/Settings/hooks/useBankingInformation.js @@ -0,0 +1,35 @@ +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/Settings/hooks/useBankingInformation.test.js b/src/Settings/hooks/useBankingInformation.test.js new file mode 100644 index 00000000..03046dfe --- /dev/null +++ b/src/Settings/hooks/useBankingInformation.test.js @@ -0,0 +1,53 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; + +import { + renderHook, + waitFor, +} from '@folio/jest-config-stripes/testing-library/react'; +import { useOkapiKy } from '@folio/stripes/core'; + +import { useBankingInformation } from './useBankingInformation'; + +const queryClient = new QueryClient(); + +const MOCK_BANKING_INFORMATION = { + 'id': 'cb007def-4b68-496c-ad78-ea8e039e819d', + 'key': 'BANKING_INFORMATION_ENABLED', + 'value': 'true', + refetch: jest.fn(), +}; + +// eslint-disable-next-line react/prop-types +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useBankingInformation', () => { + beforeEach(() => { + useOkapiKy + .mockClear() + .mockReturnValue({ + get: () => ({ + json: () => Promise.resolve({ settings: [MOCK_BANKING_INFORMATION] }), + }), + }); + }); + + it('should fetch all organization types', async () => { + const { result } = renderHook(() => useBankingInformation(), { 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, + })); + }); +}); diff --git a/translations/ui-organizations/en.json b/translations/ui-organizations/en.json index c591d8b2..ec12c26e 100644 --- a/translations/ui-organizations/en.json +++ b/translations/ui-organizations/en.json @@ -36,6 +36,7 @@ "filterConfig.contactPeopleCategory": "Contact people category", "filterConfig.country": "Country", "filterConfig.isVendor": "Is vendor", + "filterConfig.isDonor": "Is donor", "filterConfig.languages": "Languages", "filterConfig.paymentMethod": "Payment method", "filterConfig.statsAvailable": "Stats available", @@ -442,6 +443,21 @@ "settings.type": "Type", "settings.typeStatus.Active": "Active", "settings.typeStatus.Inactive": "Inactive", + "settings.bankingInformation": "Banking information", + "settings.bankingInformation.enable": "Enable banking information", + "settings.bankingAccountTypes": "Account types", + "settings.bankingAccountTypes.cannotDeleteTermHeader": "Cannot delete account type", + "settings.bankingAccountTypes.cannotDeleteTermMessage": "This account type cannot be deleted, as it is in use by one or more records.", + "settings.bankingAccountTypes.deleteEntry": "Delete account type", + "settings.bankingAccountTypes.termDeleted": "The account type {term} was successfully deleted", + "settings.bankingAccountTypes.termWillBeDeleted": "The account type {term} will be deleted.", + "settings.accountTypes.save.error.accountTypeMustBeUnique": "Account type must be unique.", + "settings.accountTypes.save.button": "Save", + "settings.accountTypes.save.success.message": "Setting was successfully updated.", + "settings.accountTypes.save.error.generic.message": "Something went wrong.", + "settings.accountTypes.name": "Name", + "settings.accountTypes.action": "Action", + "permission.view": "Organizations: View", "permission.edit": "Organizations: View, edit", "permission.create": "Organizations: View, edit, create",