From f4e9a23c6ad280253c44dd81ef7deb98c97efc6f Mon Sep 17 00:00:00 2001 From: Dmytro Melnyshyn Date: Mon, 9 Feb 2026 09:02:50 +0200 Subject: [PATCH 1/2] UICIRC-1091: Add a new field in 'Other settings' to control the display of custom fields in Checkout app. --- CHANGELOG.md | 1 + src/constants.js | 3 ++ .../CheckoutSettings/CheckoutSettings.js | 41 +++++++++++++++- .../CheckoutSettings/CheckoutSettings.test.js | 49 ++++++++++++++++++- .../CheckoutSettings/CheckoutSettingsForm.js | 13 +++++ .../CheckoutSettingsForm.test.js | 5 ++ .../CirculationSettingsConfig.js | 5 +- .../__mock__/stripesSmartComponents.mock.js | 4 ++ translations/ui-circulation/en.json | 2 + 9 files changed, 118 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebab5bc5d..6d1ca7504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * *BREAKING* Migration from mod-config and mod-settings to mod-circulation. Refs UICIRC-1247. * Update permissions after migration to mod-circulation. Refs UICIRC-1324. * Fixed an issue where the value containing the previously selected option was not reset. Refs UICIRC-1317. +* Add a new field in "Other settings" to control the display of custom fields in Checkout app. Refs UICIRC-1091. ## [11.0.4](https://github.com/folio-org/ui-circulation/tree/v11.0.4) (2024-12-10) [Full Changelog](https://github.com/folio-org/ui-circulation/compare/v11.0.3...v11.0.4) diff --git a/src/constants.js b/src/constants.js index 5cfc73774..abad40930 100644 --- a/src/constants.js +++ b/src/constants.js @@ -657,3 +657,6 @@ export const TOKEN_PROP_TYPES = PropTypes.shape({ previewValue: PropTypes.string, })), }); + +export const USERS_MODULE = 'users'; +export const CUSTOM_FIELDS_ENTITY_TYPE = 'user'; diff --git a/src/settings/CheckoutSettings/CheckoutSettings.js b/src/settings/CheckoutSettings/CheckoutSettings.js index e8875139e..ceb3c2018 100644 --- a/src/settings/CheckoutSettings/CheckoutSettings.js +++ b/src/settings/CheckoutSettings/CheckoutSettings.js @@ -1,3 +1,7 @@ +import { + useCallback, + useMemo, +} from 'react'; import { useIntl, } from 'react-intl'; @@ -5,6 +9,7 @@ import { import { TitleManager, } from '@folio/stripes/core'; +import { useCustomFieldsQuery } from '@folio/stripes/smart-components'; import { CirculationSettingsConfig, @@ -12,6 +17,8 @@ import { import CheckoutSettingsForm from './CheckoutSettingsForm'; import { CONFIG_NAMES, + USERS_MODULE, + CUSTOM_FIELDS_ENTITY_TYPE, } from '../../constants'; export const DEFAULT_INITIAL_CONFIG = { @@ -22,9 +29,10 @@ export const DEFAULT_INITIAL_CONFIG = { prefPatronIdentifier: '', useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: false, + allowedCustomFieldRefIds: [], }; -export const getInitialValues = (settings) => { +export const getInitialValues = (settings, customFieldsOptions) => { const config = { ...DEFAULT_INITIAL_CONFIG, ...settings, @@ -45,6 +53,10 @@ export const getInitialValues = (settings) => { } }); + const allowedCustomFieldRefIds = config.allowedCustomFieldRefIds.map(refId => { + return customFieldsOptions.find(customField => customField.value === refId) || { value: refId, label: refId }; + }); + return { audioAlertsEnabled: config.audioAlertsEnabled, audioTheme: config.audioTheme, @@ -53,6 +65,7 @@ export const getInitialValues = (settings) => { identifiers, useCustomFieldsAsIdentifiers: config.useCustomFieldsAsIdentifiers, wildcardLookupEnabled: config.wildcardLookupEnabled, + allowedCustomFieldRefIds, }; }; @@ -64,6 +77,7 @@ export const normalize = ({ identifiers, useCustomFieldsAsIdentifiers, wildcardLookupEnabled, + allowedCustomFieldRefIds, }) => { // As in `getInitialValues`, we must assume knowledge of how the IDs and Custom Field IDs // are rendered in CheckoutSettingsForm. IDs can be toggled on and off by a checkbox, @@ -92,6 +106,7 @@ export const normalize = ({ prefPatronIdentifier, useCustomFieldsAsIdentifiers, wildcardLookupEnabled, + allowedCustomFieldRefIds: allowedCustomFieldRefIds.map(customField => customField.value), }; }; @@ -100,6 +115,26 @@ const CheckoutSettings = () => { formatMessage, } = useIntl(); + const { + customFields, + isLoadingCustomFields, + } = useCustomFieldsQuery({ + moduleName: USERS_MODULE, + entityType: CUSTOM_FIELDS_ENTITY_TYPE, + isVisible: true, + }); + + const customFieldsOptions = useMemo(() => { + return (customFields || []).map(({ name, refId }) => ({ + label: name, + value: refId, + })); + }, [customFields]); + + const getOriginalValues = useCallback((settings) => { + return getInitialValues(settings, customFieldsOptions); + }, [customFieldsOptions]); + return ( { diff --git a/src/settings/CheckoutSettings/CheckoutSettings.test.js b/src/settings/CheckoutSettings/CheckoutSettings.test.js index 2514ca483..10a5a2e23 100644 --- a/src/settings/CheckoutSettings/CheckoutSettings.test.js +++ b/src/settings/CheckoutSettings/CheckoutSettings.test.js @@ -5,6 +5,9 @@ import { import { TitleManager, } from '@folio/stripes/core'; +import { + useCustomFieldsQuery, +} from '@folio/stripes/smart-components'; import CheckoutSettings, { getInitialValues, @@ -34,10 +37,29 @@ const labelIds = { paneTitle: 'ui-circulation.settings.index.otherSettings', }; +const customFieldsOptions = [{ + label: 'Custom Field 1', + value: 'refId1' +}, { + label: 'Custom Field 2', + value: 'refId2' +}]; + describe('CheckoutSettings', () => { beforeEach(() => { jest.clearAllMocks(); + useCustomFieldsQuery.mockReturnValue({ + customFields: [{ + name: 'Custom Field 1', + refId: 'refId1', + }, { + name: 'Custom Field 2', + refId: 'refId2', + }], + isLoadingCustomFields: false, + }); + render(); }); @@ -61,8 +83,10 @@ describe('CheckoutSettings', () => { label: labelIds.paneTitle, configName: CONFIG_NAMES.OTHER_SETTINGS, configFormComponent: CheckoutSettingsForm, - getInitialValues, + getInitialValues: expect.any(Function), onBeforeSave: normalize, + customFieldsOptions, + isLoadingCustomFields: false, }), {} ); @@ -79,6 +103,7 @@ describe('getInitialValues', () => { identifiers: { custom: [] }, useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: false, + allowedCustomFieldRefIds: [], }); expect(getInitialValues({})).toEqual({ @@ -89,6 +114,7 @@ describe('getInitialValues', () => { identifiers: { custom: [] }, useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: false, + allowedCustomFieldRefIds: [], }); }); @@ -101,9 +127,10 @@ describe('getInitialValues', () => { prefPatronIdentifier: 'barcode,customFields.cf1', useCustomFieldsAsIdentifiers: true, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: ['refId1', 'refId2'], }; - expect(getInitialValues(custom)).toEqual({ + expect(getInitialValues(custom, customFieldsOptions)).toEqual({ audioAlertsEnabled: true, audioTheme: 'modern', checkoutTimeout: false, @@ -114,6 +141,13 @@ describe('getInitialValues', () => { }, useCustomFieldsAsIdentifiers: true, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: [{ + label: 'Custom Field 1', + value: 'refId1', + }, { + label: 'Custom Field 2', + value: 'refId2', + }], }); }); }); @@ -132,6 +166,10 @@ describe('normalize', () => { }, useCustomFieldsAsIdentifiers: true, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: [{ + label: 'Custom Field 1', + value: 'refId1', + }], }; expect(normalize(input)).toEqual({ @@ -142,6 +180,7 @@ describe('normalize', () => { prefPatronIdentifier: 'barcode,username,customFields.cf1,customFields.cf2', useCustomFieldsAsIdentifiers: true, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: ['refId1'], }); }); @@ -154,6 +193,7 @@ describe('normalize', () => { identifiers: { custom: [] }, useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: false, + allowedCustomFieldRefIds: [], }; expect(normalize(input)).toEqual({ @@ -164,6 +204,7 @@ describe('normalize', () => { prefPatronIdentifier: '', useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: false, + allowedCustomFieldRefIds: [], }); }); @@ -179,6 +220,7 @@ describe('normalize', () => { }, useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: [], }; expect(normalize(input)).toEqual({ @@ -189,6 +231,7 @@ describe('normalize', () => { prefPatronIdentifier: 'barcode', useCustomFieldsAsIdentifiers: false, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: [], }); }); @@ -204,6 +247,7 @@ describe('normalize', () => { }, useCustomFieldsAsIdentifiers: true, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: [], }; expect(normalize(input)).toEqual({ @@ -214,6 +258,7 @@ describe('normalize', () => { prefPatronIdentifier: 'barcode,customFields.cf1,customFields.cf2', useCustomFieldsAsIdentifiers: true, wildcardLookupEnabled: true, + allowedCustomFieldRefIds: [], }); }); }); diff --git a/src/settings/CheckoutSettings/CheckoutSettingsForm.js b/src/settings/CheckoutSettings/CheckoutSettingsForm.js index a39ef26c2..027470330 100644 --- a/src/settings/CheckoutSettings/CheckoutSettingsForm.js +++ b/src/settings/CheckoutSettings/CheckoutSettingsForm.js @@ -7,6 +7,7 @@ import { useIntl, } from 'react-intl'; import { Field } from 'react-final-form'; +import isEqual from 'lodash/isEqual'; import stripesFinalForm from '@folio/stripes/final-form'; @@ -46,6 +47,7 @@ const CheckoutSettingsForm = ({ label, pristine, submitting, + customFieldsOptions, }) => { const { formatMessage } = useIntl(); @@ -191,6 +193,17 @@ const CheckoutSettingsForm = ({ name="wildcardLookupEnabled" type="checkbox" /> +
+ (option ? option.value : '')} + usePortal + emptyMessage={formatMessage({ id: 'ui-circulation.settings.checkout.customFieldsAtCheckout.noCustomFields' })} + /> ); diff --git a/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js b/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js index d08377e3b..6d9048dd2 100644 --- a/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js +++ b/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js @@ -33,6 +33,7 @@ describe('CheckoutSettingsForm', () => { audioTheme: 'ui-circulation.settings.checkout.audioTheme', wildcardLookup: 'ui-circulation.settings.checkout.wildcardLookup', otherSettingsFormSubmit: 'ui-circulation.settings.checkout.save', + customFieldsAtCheckout: 'ui-circulation.settings.checkout.customFieldsAtCheckout', }; const mockedInitialValues = { checkoutValues: { @@ -193,4 +194,8 @@ describe('CheckoutSettingsForm', () => { expect(screen.getByText(labelIds.otherSettingsFormSubmit)).toBeInTheDocument(); }); }); + + it('should display label for custom fields at checkout field', () => { + expect(screen.getByText(labelIds.customFieldsAtCheckout)).toBeInTheDocument(); + }); }); diff --git a/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js b/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js index 4b45b967e..a6d9b6bd8 100644 --- a/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js +++ b/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js @@ -30,6 +30,8 @@ const CirculationSettingsConfig = ({ resources, stripes, validate, + customFieldsOptions, + isLoadingCustomFields, }) => { const callout = useRef(); @@ -58,7 +60,7 @@ const CirculationSettingsConfig = ({ }); }, [resources, configName, mutator, calloutMessage, onBeforeSave]); - const isLoaded = resources?.settings?.hasLoaded; + const isLoaded = resources?.settings?.hasLoaded && !isLoadingCustomFields; if (!isLoaded) { return
; @@ -72,6 +74,7 @@ const CirculationSettingsConfig = ({ initialValues={initialValues} stripes={stripes} validate={validate} + customFieldsOptions={customFieldsOptions} onSubmit={onSave} > {children} diff --git a/test/jest/__mock__/stripesSmartComponents.mock.js b/test/jest/__mock__/stripesSmartComponents.mock.js index a887f3844..723b9a9e6 100644 --- a/test/jest/__mock__/stripesSmartComponents.mock.js +++ b/test/jest/__mock__/stripesSmartComponents.mock.js @@ -8,4 +8,8 @@ jest.mock('@folio/stripes/smart-components', () => ({ )), EntryManager: jest.fn(() => null), Settings: jest.fn(() => null), + useCustomFieldsQuery: jest.fn(() => ({ + customFields: [], + isLoadingCustomFields: false, + })), })); diff --git a/translations/ui-circulation/en.json b/translations/ui-circulation/en.json index aa1429e3f..ed3e53ac8 100644 --- a/translations/ui-circulation/en.json +++ b/translations/ui-circulation/en.json @@ -203,6 +203,8 @@ "settings.checkout.identifiers.externalSystemId": "External system ID", "settings.checkout.identifiers.id": "FOLIO record number (ID)", "settings.checkout.identifiers.username": "Username", + "settings.checkout.customFieldsAtCheckout": "Users custom fields to display at Check out", + "settings.checkout.customFieldsAtCheckout.noCustomFields": "No matching custom fields have been configured", "settings.patronNotices.categories.feeFineAction": "Manual fee/fine action (pay, waive, refund, transfer or cancel/error)", "settings.patronNotices.categories.feeFineCharge": "Manual fee/fine charge", From e4f43a32ee61217324fe1cd255b2aa81a6c37cab Mon Sep 17 00:00:00 2001 From: Dmytro Melnyshyn Date: Mon, 9 Feb 2026 09:44:26 +0200 Subject: [PATCH 2/2] add a test --- .../CheckoutSettings/CheckoutSettings.test.js | 13 +++++++++++++ .../CheckoutSettings/CheckoutSettingsForm.js | 4 ++++ .../CheckoutSettingsForm.test.js | 18 ++++++++++++++++-- .../CirculationSettingsConfig.js | 5 +++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/settings/CheckoutSettings/CheckoutSettings.test.js b/src/settings/CheckoutSettings/CheckoutSettings.test.js index 10a5a2e23..711ba5178 100644 --- a/src/settings/CheckoutSettings/CheckoutSettings.test.js +++ b/src/settings/CheckoutSettings/CheckoutSettings.test.js @@ -152,6 +152,19 @@ describe('getInitialValues', () => { }); }); +it('should handle missing custom fields in allowedCustomFieldRefIds', () => { + const custom = { + allowedCustomFieldRefIds: ['refId1', 'refId3'], + }; + + expect(getInitialValues(custom, customFieldsOptions).allowedCustomFieldRefIds).toEqual([{ + label: 'Custom Field 1', + value: 'refId1', + }, { + value: 'refId3', + label: 'refId3', + }]); +}); describe('normalize', () => { it('should normalizes identifiers to prefPatronIdentifier string', () => { const input = { diff --git a/src/settings/CheckoutSettings/CheckoutSettingsForm.js b/src/settings/CheckoutSettings/CheckoutSettingsForm.js index 027470330..235c6cec0 100644 --- a/src/settings/CheckoutSettings/CheckoutSettingsForm.js +++ b/src/settings/CheckoutSettings/CheckoutSettingsForm.js @@ -210,6 +210,10 @@ const CheckoutSettingsForm = ({ }; CheckoutSettingsForm.propTypes = { + customFieldsOptions: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, + })).isRequired, handleSubmit: PropTypes.func.isRequired, pristine: PropTypes.bool, submitting: PropTypes.bool, diff --git a/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js b/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js index 6d9048dd2..c61ad2828 100644 --- a/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js +++ b/src/settings/CheckoutSettings/CheckoutSettingsForm.test.js @@ -1,3 +1,5 @@ +import { Field } from 'react-final-form'; + import { render, screen, @@ -195,7 +197,19 @@ describe('CheckoutSettingsForm', () => { }); }); - it('should display label for custom fields at checkout field', () => { - expect(screen.getByText(labelIds.customFieldsAtCheckout)).toBeInTheDocument(); + describe('Custom fields at checkout field', () => { + it('should display a label', () => { + expect(screen.getByText(labelIds.customFieldsAtCheckout)).toBeInTheDocument(); + }); + + describe('when removing first item', () => { + it('should have the itemToString property returning option.value to display the second item as the first one', () => { + const fieldCall = Field.mock.calls.find(call => call[0]?.name === 'allowedCustomFieldRefIds'); + const itemToString = fieldCall[0].itemToString; + const option = { value: 'testValue' }; + + expect(itemToString(option)).toBe('testValue'); + }); + }); }); }); diff --git a/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js b/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js index a6d9b6bd8..d6b16ebf0 100644 --- a/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js +++ b/src/settings/components/CirculationSettingsConfig/CirculationSettingsConfig.js @@ -89,7 +89,12 @@ CirculationSettingsConfig.propTypes = { children: PropTypes.node, configFormComponent: PropTypes.elementType.isRequired, configName: PropTypes.string.isRequired, + customFieldsOptions: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.string, + })).isRequired, getInitialValues: PropTypes.func, + isLoadingCustomFields: PropTypes.bool, label: PropTypes.node.isRequired, lastMenu: PropTypes.element, mutator: PropTypes.shape({