From e02c82703b0793e4e42a96d23c3c7784ec013895 Mon Sep 17 00:00:00 2001 From: EthanFreestone <54310740+EthanFreestone@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:46:46 +0000 Subject: [PATCH] UIORGS-336 Create or change vendor code using number generator (#536) Refs UXPROD-3891, UIORGS-336, UIORGS-337 --- CHANGELOG.md | 1 + package.json | 24 +++- .../FieldCode/FieldCode.js | 57 +++++++-- .../OrganizationSummaryForm.test.js | 60 ++++++++- .../NumberGeneratorSettings.js | 52 ++++++++ .../NumberGeneratorSettings.test.js | 117 ++++++++++++++++++ .../NumberGeneratorSettingsForm.css | 5 + .../NumberGeneratorSettingsForm.js | 101 +++++++++++++++ .../NumberGeneratorSettingsForm.test.js | 63 ++++++++++ src/Settings/NumberGeneratorSettings/index.js | 1 + src/Settings/SettingsPage.js | 24 +++- src/Settings/SettingsPage.test.js | 18 ++- src/common/constants/index.js | 1 + src/common/constants/numberGenerator.js | 11 ++ src/common/hooks/index.js | 1 + .../useVendorCodeGeneratorSettings/index.js | 1 + .../useVendorCodeGeneratorSettings.js | 44 +++++++ .../useVendorCodeGeneratorSettings.test.js | 74 +++++++++++ translations/ui-organizations/en.json | 9 ++ 19 files changed, 647 insertions(+), 17 deletions(-) create mode 100644 src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.js create mode 100644 src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.test.js create mode 100644 src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.css create mode 100644 src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.js create mode 100644 src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.test.js create mode 100644 src/Settings/NumberGeneratorSettings/index.js create mode 100644 src/common/constants/numberGenerator.js create mode 100644 src/common/hooks/useVendorCodeGeneratorSettings/index.js create mode 100644 src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.js create mode 100644 src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef5558f..0b454b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Adapt organization metadata fields to version history mechanism. Refs UIORGS-359. * Add claiming to organization integration details. Refs UIORGS-442. * Add "Duplicate" integration action to organization integration view. Refs UIORGS-441. +* *BREAKING* Add number generator for vendor code including settings page. Refs UIORGS-336, UIORGS-337. ## [5.2.0](https://github.com/folio-org/ui-organizations/tree/v5.2.0) (2024-10-31) [Full Changelog](https://github.com/folio-org/ui-organizations/compare/v5.1.1...v5.2.0) diff --git a/package.json b/package.json index 9ca265e0..c0db48c3 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,14 @@ "organizations-storage.interfaces": "2.1", "organizations-storage.phone-numbers": "2.0", "organizations-storage.privileged-contacts": "1.0", - "organizations-storage.settings": "1.0", + "organizations-storage.settings": "1.1", "organizations-storage.urls": "1.1", "tags": "1.0", "users": "15.1 16.0" }, + "optionalOkapiInterfaces": { + "servint": "4.0" + }, "queryResource": "query", "icons": [ { @@ -46,6 +49,7 @@ } ], "stripesDeps": [ + "@folio/service-interaction", "@folio/stripes-acq-components" ], "permissionSets": [ @@ -64,7 +68,9 @@ { "permissionName": "ui-organizations.third-party-services.execute", "displayName": "Organizations: Permissions required to call services apart from mod-organizations-storage", - "replaces": ["ui-organizations.third-party-services"], + "replaces": [ + "ui-organizations.third-party-services" + ], "visible": false, "subPermissions": [ "acquisition.organization.events.get", @@ -297,6 +303,17 @@ "organizations-storage.banking-account-types.item.put", "organizations-storage.banking-account-types.item.delete" ] + }, + { + "permissionName": "ui-organizations.settings.numberGenerator.manage", + "displayName": "Settings (Organizations): Manage number generator options", + "subPermissions": [ + "settings.organizations.enabled", + "organizations-storage.settings.collection.get", + "organizations-storage.settings.item.post", + "organizations-storage.settings.item.put" + ], + "visible": true } ] }, @@ -327,8 +344,10 @@ "@bigtest/react": "^0.1.2", "@folio/eslint-config-stripes": "^7.0.0", "@folio/jest-config-stripes": "^2.0.0", + "@folio/service-interaction": "^3.1.0", "@folio/stripes": "^9.0.0", "@folio/stripes-cli": "^3.0.0", + "@folio/stripes-erm-components": "^9.2.0", "@formatjs/cli": "^6.1.3", "babel-jest": "^26.3.0", "chai": "^4.2.0", @@ -367,6 +386,7 @@ "redux-form": "^8.3.0" }, "peerDependencies": { + "@folio/service-interaction": "^3.1.0", "@folio/stripes": "^9.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/Organizations/OrganizationForm/OrganizationSummaryForm/FieldCode/FieldCode.js b/src/Organizations/OrganizationForm/OrganizationSummaryForm/FieldCode/FieldCode.js index 201e31e1..e3c1fc1c 100644 --- a/src/Organizations/OrganizationForm/OrganizationSummaryForm/FieldCode/FieldCode.js +++ b/src/Organizations/OrganizationForm/OrganizationSummaryForm/FieldCode/FieldCode.js @@ -1,30 +1,67 @@ import React, { useCallback } from 'react'; -import { Field } from 'react-final-form'; +import { + Field, + useForm, +} from 'react-final-form'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; +import { NumberGeneratorModalButton } from '@folio/service-interaction'; import { stripesConnect } from '@folio/stripes/core'; -import { TextField } from '@folio/stripes/components'; +import { + Col, + Row, + TextField, +} from '@folio/stripes/components'; +import { VENDOR_CODE_GENERATOR_CODE } from '../../../../common/constants'; +import { useVendorCodeGeneratorSettings } from '../../../../common/hooks'; import { fetchOrgsByParam } from '../../../../common/resources'; import { validateOrgCode } from './validateOrgCode'; const FieldCode = ({ orgId, mutator }) => { + const { change, resetFieldState } = useForm(); + const { isUseGenerator, isUseBoth } = useVendorCodeGeneratorSettings(); + const validate = useCallback(value => { return validateOrgCode(mutator.fetchOrgByCode, orgId, value); }, // eslint-disable-next-line react-hooks/exhaustive-deps [orgId]); + const handleGeneratedValue = useCallback((generatedValue) => { + change('code', generatedValue); + resetFieldState('code'); + }, [change, resetFieldState]); + return ( - } - name="code" - required - validate={validate} - /> + + + } + name="code" + required + validate={validate} + /> + + {(isUseGenerator || isUseBoth) && ( + + } + callback={handleGeneratedValue} + generateButtonLabel={} + generator={VENDOR_CODE_GENERATOR_CODE} + id="vendor-code-generator" + modalProps={{ + label: , + }} + /> + + )} + ); }; diff --git a/src/Organizations/OrganizationForm/OrganizationSummaryForm/OrganizationSummaryForm.test.js b/src/Organizations/OrganizationForm/OrganizationSummaryForm/OrganizationSummaryForm.test.js index 2c074b32..32843e0e 100644 --- a/src/Organizations/OrganizationForm/OrganizationSummaryForm/OrganizationSummaryForm.test.js +++ b/src/Organizations/OrganizationForm/OrganizationSummaryForm/OrganizationSummaryForm.test.js @@ -1,4 +1,5 @@ import React from 'react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; import { MemoryRouter } from 'react-router-dom'; @@ -8,10 +9,25 @@ import { organization, organizationTypes } from 'fixtures'; import { QueryClient, QueryClientProvider } from 'react-query'; import OrganizationSummaryForm from './OrganizationSummaryForm'; -import { useTypes } from '../../../common/hooks'; +import { + useTypes, + useVendorCodeGeneratorSettings, +} from '../../../common/hooks'; jest.mock('../../../common/hooks', () => ({ useTypes: jest.fn(), + useVendorCodeGeneratorSettings: jest.fn(), +})); + +jest.mock('@folio/service-interaction', () => ({ + NumberGeneratorModalButton: ({ callback }) => ( + + ), })); const TestForm = stripesFinalForm({})( @@ -44,6 +60,11 @@ describe('OrganizationSummaryForm', () => { useTypes .mockClear() .mockReturnValue({ organizationTypes, totalRecords: organizationTypes.length }); + useVendorCodeGeneratorSettings.mockReturnValue({ + isUseGenerator: false, + isUseBoth: false, + isUseTextfield: false, + }); }); afterEach(() => { @@ -73,4 +94,41 @@ describe('OrganizationSummaryForm', () => { expect((selectedType).length).toEqual(1); }); + + it('should not render the NumberGeneratorModalButton', async () => { + renderForm(); + + expect(screen.getByRole('textbox', { name: 'ui-organizations.summary.code' })).toBeEnabled(); + expect(screen.queryByRole('button', { name: 'NumberGeneratorModalButton' })).not.toBeInTheDocument(); + }); + + it('should render the NumberGeneratorModalButton when setting=isUseGenerator', async () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isUseGenerator: true }); + + renderForm(); + + expect(screen.getByRole('textbox', { name: 'ui-organizations.summary.code' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'NumberGeneratorModalButton' })).toBeInTheDocument(); + }); + + it('should render the NumberGeneratorModalButton when setting=isUseBoth', async () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isUseBoth: true }); + + renderForm(); + + expect(screen.getByRole('textbox', { name: 'ui-organizations.summary.code' })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'NumberGeneratorModalButton' })).toBeInTheDocument(); + }); + + it('should update vendor code field value when NumberGeneratorModalButton is clicked', async () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isUseBoth: true }); + + renderForm(); + const button = screen.getByRole('button', { name: 'NumberGeneratorModalButton' }); + const input = screen.getByRole('textbox', { name: 'ui-organizations.summary.code' }); + + expect(input).toHaveValue(''); + await userEvent.click(button); + expect(input).toHaveValue('abc123'); + }); }); diff --git a/src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.js b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.js new file mode 100644 index 00000000..442278b8 --- /dev/null +++ b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.js @@ -0,0 +1,52 @@ +import { FormattedMessage } from 'react-intl'; + +import { Loading } from '@folio/stripes/components'; +import { useOkapiKy } from '@folio/stripes/core'; +import { useShowCallout } from '@folio/stripes-acq-components'; + +import NumberGeneratorSettingsForm from './NumberGeneratorSettingsForm'; +import { SETTINGS_API } from '../../common/constants/api'; +import { VENDOR_CODE_GENERATOR_SETTINGS_KEY } from '../../common/constants/numberGenerator'; +import { useVendorCodeGeneratorSettings } from '../../common/hooks/useVendorCodeGeneratorSettings'; + +const NumberGeneratorSettings = () => { + const { vendorCodeSetting, isLoading } = useVendorCodeGeneratorSettings(); + const ky = useOkapiKy(); + const sendCallout = useShowCallout(); + + const onSubmit = async ({ [VENDOR_CODE_GENERATOR_SETTINGS_KEY]: value }) => { + try { + if (vendorCodeSetting) { + await ky.put(`${SETTINGS_API}/${vendorCodeSetting.id}`, { + json: { ...vendorCodeSetting, value }, + }); + } else { + await ky.post(SETTINGS_API, { + json: { key: VENDOR_CODE_GENERATOR_SETTINGS_KEY, value }, + }); + } + + sendCallout({ + message: , + }); + } catch (error) { + sendCallout({ + type: 'error', + message: , + }); + } + }; + + if (isLoading) { + return ; + } + + return ( + + ); +}; + +export default NumberGeneratorSettings; diff --git a/src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.test.js b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.test.js new file mode 100644 index 00000000..1f5bc26f --- /dev/null +++ b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettings.test.js @@ -0,0 +1,117 @@ +import { FormattedMessage } from 'react-intl'; + +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; +import { useOkapiKy } from '@folio/stripes/core'; +import { useShowCallout } from '@folio/stripes-acq-components'; + +import NumberGeneratorSettings from './NumberGeneratorSettings'; +import { SETTINGS_API } from '../../common/constants/api'; +import { useVendorCodeGeneratorSettings } from '../../common/hooks/useVendorCodeGeneratorSettings'; + +jest.mock('@folio/stripes/core', () => ({ + useOkapiKy: jest.fn(), +})); + +jest.mock('@folio/stripes-acq-components', () => ({ + useShowCallout: jest.fn(), +})); + +jest.mock('../../common/hooks/useVendorCodeGeneratorSettings'); + +jest.mock('@folio/stripes/components', () => ({ + Loading: () =>
Loading
, +})); + +jest.mock('../../common/constants/numberGenerator', () => ({ + ...jest.requireActual('../../common/constants/numberGenerator'), + VENDOR_CODE_GENERATOR_SETTINGS_KEY: 'testKey', +})); + +jest.mock('./NumberGeneratorSettingsForm', () => jest.fn(({ onSubmit }) => ( + +))); + +const renderComponent = () => render(); + +describe('NumberGeneratorSettings', () => { + const mockKyPut = jest.fn(); + const mockKyPost = jest.fn(); + const mockSendCallout = jest.fn(); + + beforeEach(() => { + useOkapiKy.mockReturnValue({ + put: mockKyPut, + post: mockKyPost, + }); + useShowCallout.mockReturnValue(mockSendCallout); + }); + + it('should render Loading', () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isLoading: true }); + renderComponent(); + expect(screen.getByText('Loading')).toBeInTheDocument(); + }); + + it('should render NumberGeneratorSettingsForm', () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isLoading: false }); + renderComponent(); + expect(screen.getByText('Submit')).toBeInTheDocument(); + }); + + it('should call ky.post when no setting is present', async () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isLoading: false }); + renderComponent(); + + await userEvent.click(screen.getByText('Submit')); + + expect(mockKyPost).toHaveBeenCalledWith(SETTINGS_API, { + json: { key: 'testKey', value: 'testValue' }, + }); + expect(mockSendCallout).toHaveBeenCalledWith({ + message: , + }); + }); + + it('should call ky.put when setting exists', async () => { + const vendorCodeSetting = { id: '123', key: 'testKey', value: 'someValue', _version: 1 }; + + useVendorCodeGeneratorSettings.mockReturnValue({ + isLoading: false, + vendorCodeSetting, + }); + renderComponent(); + + await userEvent.click(screen.getByText('Submit')); + + expect(mockKyPut).toHaveBeenCalledWith(`${SETTINGS_API}/${vendorCodeSetting.id}`, { + json: { ...vendorCodeSetting, value: 'testValue' }, + }); + expect(mockSendCallout).toHaveBeenCalledWith({ + message: , + }); + }); + + it('should show error callout when submission fails', async () => { + useVendorCodeGeneratorSettings.mockReturnValue({ isLoading: false }); + mockKyPost.mockRejectedValue(new Error()); + renderComponent(); + + await userEvent.click(screen.getByText('Submit')); + + expect(mockSendCallout).toHaveBeenCalledWith({ + type: 'error', + message: , + }); + }); +}); diff --git a/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.css b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.css new file mode 100644 index 00000000..78ddf841 --- /dev/null +++ b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.css @@ -0,0 +1,5 @@ +@import '@folio/stripes-components/lib/variables'; + +.marginBottomGutter { + margin-bottom: var(--gutter); +} diff --git a/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.js b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.js new file mode 100644 index 00000000..3f43e906 --- /dev/null +++ b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.js @@ -0,0 +1,101 @@ +import PropTypes from 'prop-types'; +import { Field } from 'react-final-form'; +import { FormattedMessage } from 'react-intl'; + +import { + Button, + Col, + MessageBanner, + Pane, + PaneFooter, + PaneHeader, + RadioButton, + Row, +} from '@folio/stripes/components'; +import stripesFinalForm from '@folio/stripes/final-form'; + +import css from './NumberGeneratorSettingsForm.css'; +import { + VENDOR_CODE_GENERATOR_OPTIONS, + VENDOR_CODE_GENERATOR_SETTINGS_KEY, +} from '../../common/constants/numberGenerator'; + +const NumberGeneratorSettingsForm = ({ handleSubmit, pristine, submitting }) => { + const paneHeader = (renderProps) => ( + } + /> + ); + + const paneFooter = ( + + + + } + /> + ); + + return ( + + + +
+ + + +
+ +
+ + + } + name={VENDOR_CODE_GENERATOR_SETTINGS_KEY} + type="radio" + value={VENDOR_CODE_GENERATOR_OPTIONS.TEXTFIELD} + /> + } + name={VENDOR_CODE_GENERATOR_SETTINGS_KEY} + type="radio" + value={VENDOR_CODE_GENERATOR_OPTIONS.BOTH} + /> + } + name={VENDOR_CODE_GENERATOR_SETTINGS_KEY} + type="radio" + value={VENDOR_CODE_GENERATOR_OPTIONS.GENERATOR} + /> + + +
+ ); +}; + +NumberGeneratorSettingsForm.propTypes = { + handleSubmit: PropTypes.func.isRequired, + pristine: PropTypes.bool.isRequired, + submitting: PropTypes.bool.isRequired, +}; + +export default stripesFinalForm({ + enableReinitialize: true, + keepDirtyOnReinitialize: true, + navigationCheck: true, + subscription: { values: true }, +})(NumberGeneratorSettingsForm); diff --git a/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.test.js b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.test.js new file mode 100644 index 00000000..81845d9d --- /dev/null +++ b/src/Settings/NumberGeneratorSettings/NumberGeneratorSettingsForm.test.js @@ -0,0 +1,63 @@ +import { MemoryRouter } from 'react-router-dom'; + +import { + render, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; + +import NumberGeneratorSettingsForm from './NumberGeneratorSettingsForm'; +import { + VENDOR_CODE_GENERATOR_OPTIONS, + VENDOR_CODE_GENERATOR_SETTINGS_KEY, +} from '../../common/constants/numberGenerator'; + +jest.mock('@folio/stripes/core', () => ({ + useStripes: jest.fn(), +})); + +const onSubmitMock = jest.fn(); + +const renderComponent = () => render( + onSubmitMock(values)} + />, + { wrapper: MemoryRouter }, +); + +describe('NumberGeneratorSettingsForm', () => { + it('should render the component with initial values', () => { + renderComponent(); + + expect(screen.getByText('ui-organizations.settings.numberGeneratorOptions')).toBeInTheDocument(); + expect(screen.getByText('ui-organizations.settings.numberGeneratorOptions.info')).toBeInTheDocument(); + expect( + screen.getByLabelText('ui-organizations.settings.numberGeneratorOptions.useTextFieldForVendor'), + ).toBeInTheDocument(); + expect( + screen.getByLabelText('ui-organizations.settings.numberGeneratorOptions.useBothForVendor'), + ).toBeChecked(); + expect( + screen.getByLabelText('ui-organizations.settings.numberGeneratorOptions.useGeneratorForVendor'), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'stripes-core.button.save' })).toBeDisabled(); + }); + + it('should call onSubmit with correct values', async () => { + renderComponent(); + + const textfieldRadioButton = screen.getByRole('radio', { + name: 'ui-organizations.settings.numberGeneratorOptions.useTextFieldForVendor', + }); + const saveButton = screen.getByRole('button', { name: 'stripes-core.button.save' }); + + await userEvent.click(textfieldRadioButton); + expect(saveButton).toBeEnabled(); + + await userEvent.click(saveButton); + expect(onSubmitMock).toHaveBeenCalledWith({ + [VENDOR_CODE_GENERATOR_SETTINGS_KEY]: VENDOR_CODE_GENERATOR_OPTIONS.TEXTFIELD, + }); + }); +}); diff --git a/src/Settings/NumberGeneratorSettings/index.js b/src/Settings/NumberGeneratorSettings/index.js new file mode 100644 index 00000000..106b27f9 --- /dev/null +++ b/src/Settings/NumberGeneratorSettings/index.js @@ -0,0 +1 @@ +export { default as NumberGeneratorSettings } from './NumberGeneratorSettings'; diff --git a/src/Settings/SettingsPage.js b/src/Settings/SettingsPage.js index 833afab1..71efcd20 100644 --- a/src/Settings/SettingsPage.js +++ b/src/Settings/SettingsPage.js @@ -3,11 +3,15 @@ import { FormattedMessage } from 'react-intl'; import { Settings } from '@folio/stripes/smart-components'; -import { useBankingInformationSettings } from '../common/hooks'; +import { + useBankingInformationSettings, + useVendorCodeGeneratorSettings, +} from '../common/hooks'; import { CategorySettings } from './CategorySettings'; import { TypeSettings } from './TypeSettings'; import { BankingAccountTypeSettings } from './BankingAccountTypeSettings'; import { BankingInformationSettings } from './BankingInformationSettings'; +import { NumberGeneratorSettings } from './NumberGeneratorSettings'; const pages = [ { @@ -37,10 +41,24 @@ const bankingAccountTypesPage = { route: 'banking-account-types', }; +const numberGeneratorOptionsPage = { + component: NumberGeneratorSettings, + label: , + perm: 'ui-organizations.settings.numberGenerator.manage', + route: 'numberGeneratorOptions', +}; + const SettingsPage = (props) => { - const { enabled } = useBankingInformationSettings(); + const { enabled: bankingInformationEnabled } = useBankingInformationSettings(); + const { enabled: numberGeneratorEnabled } = useVendorCodeGeneratorSettings(); - const settingsPages = useMemo(() => (enabled ? pages.concat(bankingAccountTypesPage) : pages), [enabled]); + const settingsPages = useMemo(() => { + return [ + ...pages, + ...(bankingInformationEnabled ? [bankingAccountTypesPage] : []), + ...(numberGeneratorEnabled ? [numberGeneratorOptionsPage] : []), + ]; + }, [bankingInformationEnabled, numberGeneratorEnabled]); return ( ({ isLoading: false, enabled: false, })), + useVendorCodeGeneratorSettings: jest.fn(() => ({ + enabled: false, + })), })); const stripesMock = { @@ -49,6 +55,8 @@ describe('SettingsPage', () => { expect(screen.getByText('ui-organizations.settings.categories')).toBeInTheDocument(); expect(screen.getByText('ui-organizations.settings.types')).toBeInTheDocument(); expect(screen.getByText('ui-organizations.settings.bankingInformation')).toBeInTheDocument(); + expect(screen.queryByText('ui-organizations.settings.bankingAccountTypes')).not.toBeInTheDocument(); + expect(screen.queryByText('ui-organizations.settings.numberGeneratorOptions')).not.toBeInTheDocument(); }); it('should return banking account types link', async () => { @@ -61,4 +69,12 @@ describe('SettingsPage', () => { expect(screen.getByText('ui-organizations.settings.bankingAccountTypes')).toBeInTheDocument(); }); + + it('should return numberGeneratorOptions link', async () => { + useVendorCodeGeneratorSettings.mockReturnValueOnce({ enabled: true }); + + renderSettingsPage(); + + expect(screen.getByText('ui-organizations.settings.numberGeneratorOptions')).toBeInTheDocument(); + }); }); diff --git a/src/common/constants/index.js b/src/common/constants/index.js index 52453f15..36d718ed 100644 --- a/src/common/constants/index.js +++ b/src/common/constants/index.js @@ -2,6 +2,7 @@ export * from './api'; export * from './categories'; export * from './events'; export * from './interfaces'; +export * from './numberGenerator'; export * from './organization'; export * from './organizationTypes'; export * from './routes'; diff --git a/src/common/constants/numberGenerator.js b/src/common/constants/numberGenerator.js new file mode 100644 index 00000000..d18894b9 --- /dev/null +++ b/src/common/constants/numberGenerator.js @@ -0,0 +1,11 @@ +export const NUMBER_GENERATOR_INTERFACE_NAME = 'servint'; +export const NUMBER_GENERATOR_INTERFACE_VERSION = '4.0'; + +export const VENDOR_CODE_GENERATOR_OPTIONS = { + BOTH: 'useBoth', + TEXTFIELD: 'useTextField', + GENERATOR: 'useGenerator', +}; + +export const VENDOR_CODE_GENERATOR_CODE = 'organizations_vendorCode'; +export const VENDOR_CODE_GENERATOR_SETTINGS_KEY = 'vendor-code-generator-setting'; diff --git a/src/common/hooks/index.js b/src/common/hooks/index.js index d866e888..14bfc2c0 100644 --- a/src/common/hooks/index.js +++ b/src/common/hooks/index.js @@ -12,4 +12,5 @@ export * from './useLinkedAgreements'; export * from './useOrganizationBankingInformation'; export * from './useTranslatedCategories'; export * from './useTypes'; +export * from './useVendorCodeGeneratorSettings'; export * from './useVersionWrappedRowFormatter'; diff --git a/src/common/hooks/useVendorCodeGeneratorSettings/index.js b/src/common/hooks/useVendorCodeGeneratorSettings/index.js new file mode 100644 index 00000000..cd975bcd --- /dev/null +++ b/src/common/hooks/useVendorCodeGeneratorSettings/index.js @@ -0,0 +1 @@ +export { useVendorCodeGeneratorSettings } from './useVendorCodeGeneratorSettings'; diff --git a/src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.js b/src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.js new file mode 100644 index 00000000..a2934c8a --- /dev/null +++ b/src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.js @@ -0,0 +1,44 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, + useStripes, +} from '@folio/stripes/core'; + +import { SETTINGS_API } from '../../constants/api'; +import { + NUMBER_GENERATOR_INTERFACE_NAME, + NUMBER_GENERATOR_INTERFACE_VERSION, + VENDOR_CODE_GENERATOR_OPTIONS, + VENDOR_CODE_GENERATOR_SETTINGS_KEY, +} from '../../constants/numberGenerator'; + +const { BOTH, TEXTFIELD, GENERATOR } = VENDOR_CODE_GENERATOR_OPTIONS; + +export const useVendorCodeGeneratorSettings = () => { + const ky = useOkapiKy(); + const stripes = useStripes(); + const enabled = Boolean(stripes.hasInterface(NUMBER_GENERATOR_INTERFACE_NAME, NUMBER_GENERATOR_INTERFACE_VERSION)); + const [namespace] = useNamespace({ key: VENDOR_CODE_GENERATOR_SETTINGS_KEY }); + + const searchParams = { + query: `key==${VENDOR_CODE_GENERATOR_SETTINGS_KEY}`, + limit: 1, + }; + const queryFn = ({ signal }) => ky.get(SETTINGS_API, { searchParams, signal }).json(); + const { data, isFetching, isLoading } = useQuery([namespace], queryFn, { enabled }); + + const vendorCodeSetting = data?.settings?.[0]; + const settingValue = vendorCodeSetting?.value; + + return { + vendorCodeSetting, + isUseGenerator: settingValue === GENERATOR, + isUseTextField: settingValue === TEXTFIELD, + isUseBoth: settingValue === BOTH, + enabled, + isFetching, + isLoading, + }; +}; diff --git a/src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.test.js b/src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.test.js new file mode 100644 index 00000000..9ef21876 --- /dev/null +++ b/src/common/hooks/useVendorCodeGeneratorSettings/useVendorCodeGeneratorSettings.test.js @@ -0,0 +1,74 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; + +import { + renderHook, + waitFor, +} from '@folio/jest-config-stripes/testing-library/react'; +import { + useOkapiKy, + useStripes, +} from '@folio/stripes/core'; + +import { useVendorCodeGeneratorSettings } from './useVendorCodeGeneratorSettings'; +import { + VENDOR_CODE_GENERATOR_OPTIONS, + VENDOR_CODE_GENERATOR_SETTINGS_KEY, +} from '../../constants'; + +const settingsEntity = { + id: '3297a4ed-2071-4455-8874-23ff88029490', + key: VENDOR_CODE_GENERATOR_SETTINGS_KEY, + value: VENDOR_CODE_GENERATOR_OPTIONS.GENERATOR, +}; + +const mockKy = { + get: jest.fn(() => ({ + json: jest.fn(() => Promise.resolve({ + settings: [settingsEntity], + })), + })), +}; + +const mockStripes = { + hasInterface: jest.fn(() => true), +}; + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => {children}; + +describe('useVendorCodeGeneratorSettings', () => { + beforeEach(() => { + jest.clearAllMocks(); + queryClient.clear(); + useOkapiKy.mockReturnValue(mockKy); + useStripes.mockReturnValue(mockStripes); + }); + + it('should return correct values', async () => { + const { result } = renderHook(() => useVendorCodeGeneratorSettings(), { wrapper }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(mockKy.get).toHaveBeenCalled(); + expect(result.current.vendorCodeSetting).toEqual(settingsEntity); + expect(result.current.isUseGenerator).toBe(true); + expect(result.current.isUseTextField).toBe(false); + expect(result.current.isUseBoth).toBe(false); + expect(result.current.enabled).toBe(true); + }); + + it('should return enabled=false if interface not present', async () => { + mockStripes.hasInterface.mockImplementationOnce(() => false); + const { result } = renderHook(() => useVendorCodeGeneratorSettings(), { wrapper }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + expect(mockKy.get).not.toHaveBeenCalled(); + expect(result.current.vendorCodeSetting).toBeUndefined(); + expect(result.current.isUseGenerator).toBe(false); + expect(result.current.isUseTextField).toBe(false); + expect(result.current.isUseBoth).toBe(false); + expect(result.current.enabled).toBe(false); + }); +}); diff --git a/translations/ui-organizations/en.json b/translations/ui-organizations/en.json index 181a75c9..152e757d 100644 --- a/translations/ui-organizations/en.json +++ b/translations/ui-organizations/en.json @@ -512,6 +512,15 @@ "settings.type": "Type", "settings.typeStatus.Active": "Active", "settings.typeStatus.Inactive": "Inactive", + "settings.numberGeneratorOptions": "Number generator options", + "settings.numberGeneratorOptions.info": "Fields which are usually filled using a numeric sequence can use the number generator. When the generator is switched on the field can either be fixed to prevent manual update, or made fully editable. When switched off, the field must be filled manually.", + "settings.numberGeneratorOptions.useGeneratorForVendor": "Number generator on, fixed: the vendor code can be filled using the generator only.", + "settings.numberGeneratorOptions.useTextFieldForVendor": "Number generator off: the vendor code can be filled manually only.", + "settings.numberGeneratorOptions.useBothForVendor": "Number generator on, editable: the vendor code can be filled using the generator and be edited, or filled manually.", + "settings.numberGeneratorOptions.save.error": "Error saving setting", + "settings.numberGeneratorOptions.save.success": "Setting was successfully saved", + "numberGenerator.vendorCodeGenerator": "Vendor code generator", + "numberGenerator.generateVendorCode": "Generate vendor code", "settings.bankingInformation": "Banking information", "settings.bankingInformation.enable": "Enable banking information", "settings.bankingAccountTypes": "Account types",