diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebab5bc5..6d1ca750 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 5cfc7377..abad4093 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 e8875139..ceb3c201 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 2514ca48..711ba517 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,10 +141,30 @@ describe('getInitialValues', () => {
},
useCustomFieldsAsIdentifiers: true,
wildcardLookupEnabled: true,
+ allowedCustomFieldRefIds: [{
+ label: 'Custom Field 1',
+ value: 'refId1',
+ }, {
+ label: 'Custom Field 2',
+ value: 'refId2',
+ }],
});
});
});
+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 = {
@@ -132,6 +179,10 @@ describe('normalize', () => {
},
useCustomFieldsAsIdentifiers: true,
wildcardLookupEnabled: true,
+ allowedCustomFieldRefIds: [{
+ label: 'Custom Field 1',
+ value: 'refId1',
+ }],
};
expect(normalize(input)).toEqual({
@@ -142,6 +193,7 @@ describe('normalize', () => {
prefPatronIdentifier: 'barcode,username,customFields.cf1,customFields.cf2',
useCustomFieldsAsIdentifiers: true,
wildcardLookupEnabled: true,
+ allowedCustomFieldRefIds: ['refId1'],
});
});
@@ -154,6 +206,7 @@ describe('normalize', () => {
identifiers: { custom: [] },
useCustomFieldsAsIdentifiers: false,
wildcardLookupEnabled: false,
+ allowedCustomFieldRefIds: [],
};
expect(normalize(input)).toEqual({
@@ -164,6 +217,7 @@ describe('normalize', () => {
prefPatronIdentifier: '',
useCustomFieldsAsIdentifiers: false,
wildcardLookupEnabled: false,
+ allowedCustomFieldRefIds: [],
});
});
@@ -179,6 +233,7 @@ describe('normalize', () => {
},
useCustomFieldsAsIdentifiers: false,
wildcardLookupEnabled: true,
+ allowedCustomFieldRefIds: [],
};
expect(normalize(input)).toEqual({
@@ -189,6 +244,7 @@ describe('normalize', () => {
prefPatronIdentifier: 'barcode',
useCustomFieldsAsIdentifiers: false,
wildcardLookupEnabled: true,
+ allowedCustomFieldRefIds: [],
});
});
@@ -204,6 +260,7 @@ describe('normalize', () => {
},
useCustomFieldsAsIdentifiers: true,
wildcardLookupEnabled: true,
+ allowedCustomFieldRefIds: [],
};
expect(normalize(input)).toEqual({
@@ -214,6 +271,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 a39ef26c..235c6cec 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,12 +193,27 @@ const CheckoutSettingsForm = ({
name="wildcardLookupEnabled"
type="checkbox"
/>
+
+ (option ? option.value : '')}
+ usePortal
+ emptyMessage={formatMessage({ id: 'ui-circulation.settings.checkout.customFieldsAtCheckout.noCustomFields' })}
+ />
);
};
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 d08377e3..c61ad282 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,
@@ -33,6 +35,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 +196,20 @@ describe('CheckoutSettingsForm', () => {
expect(screen.getByText(labelIds.otherSettingsFormSubmit)).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 4b45b967..d6b16ebf 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}
@@ -86,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({
diff --git a/test/jest/__mock__/stripesSmartComponents.mock.js b/test/jest/__mock__/stripesSmartComponents.mock.js
index a887f384..723b9a9e 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 aa1429e3..ed3e53ac 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",