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",