diff --git a/package.json b/package.json index 783b87883..a9745985d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "loan-storage": "4.0 5.0 6.0 7.0", "notes": "2.0 3.0", "request-storage": "2.5 3.0 4.0 5.0 6.0", + "servint": "2.0 3.0", "batch-print": "1.0" }, "permissionSets": [ @@ -981,6 +982,13 @@ ], "visible": true }, + { + "permissionName": "ui-users.settings.numberGenerator.manage", + "displayName": "Settings (Users): Manage number generator options", + "subPermissions": [ + "settings.users.enabled" + ] + }, { "permissionName": "ui-users.profile-pictures.view", "displayName": "Users: Can view profile pictures", @@ -1079,6 +1087,7 @@ "@babel/preset-react": "^7.9.0", "@folio/eslint-config-stripes": "^7.0.0", "@folio/jest-config-stripes": "^2.0.0", + "@folio/service-interaction": "^3.0.0", "@folio/stripes": "^9.0.0", "@folio/stripes-cli": "^3.0.0", "@formatjs/cli": "^6.1.3", @@ -1118,6 +1127,7 @@ "uuid": "^9.0.0" }, "peerDependencies": { + "@folio/service-interaction": "^3.0.0", "@folio/stripes": "^9.0.0", "moment": "^2.24.0", "react": "^18.2.0", diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.js b/src/components/EditSections/EditUserInfo/EditUserInfo.js index 1311319ab..eb1868af5 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.js @@ -6,6 +6,9 @@ import { Field } from 'react-final-form'; import { OnChange } from 'react-final-form-listeners'; import { FormattedMessage, injectIntl } from 'react-intl'; +import { ViewMetaData } from '@folio/stripes/smart-components'; +import { NumberGeneratorModalButton } from '@folio/service-interaction'; + import { Button, Select, @@ -18,7 +21,6 @@ import { Modal, ModalFooter, } from '@folio/stripes/components'; -import { ViewMetaData } from '@folio/stripes/smart-components'; import { USER_TYPES, USER_TYPE_FIELD } from '../../../constants'; import { isConsortiumEnabled } from '../../util'; @@ -37,7 +39,9 @@ class EditUserInfo extends React.Component { intl: PropTypes.object.isRequired, onToggle: PropTypes.func, patronGroups: PropTypes.arrayOf(PropTypes.object), + settings: PropTypes.arrayOf(PropTypes.object), stripes: PropTypes.shape({ + hasInterface: PropTypes.func.isRequired, timezone: PropTypes.string.isRequired, store: PropTypes.shape({ dispatch: PropTypes.func.isRequired, @@ -118,19 +122,27 @@ class EditUserInfo extends React.Component { render() { const { + disabled, patronGroups, initialValues, expanded, onToggle, accordionId, intl, - stripes, uniquenessValidator, - disabled, + form: { change }, + settings, + stripes, profilePictureConfig, form, } = this.props; + let barcodeGeneratorSetting = 'useTextField'; + if (stripes.hasInterface('servint')) { + const numberGeneratorSettings = JSON.parse((settings?.find(sett => sett.configName === 'number_generator') ?? { value: '{}' }).value); + barcodeGeneratorSetting = numberGeneratorSettings?.barcodeGeneratorSetting ?? 'useTextField'; + } + const isConsortium = isConsortiumEnabled(stripes); const { barcode } = initialValues; @@ -374,8 +386,25 @@ class EditUserInfo extends React.Component { component={TextField} validate={asyncValidateField('barcode', barcode, uniquenessValidator)} fullWidth - disabled={disabled} + disabled={disabled || barcodeGeneratorSetting === 'useGenerator'} /> + {( + barcodeGeneratorSetting === 'useGenerator' || + barcodeGeneratorSetting === 'useBoth' + ) && + + } + callback={(generated) => change('barcode', generated)} + id="userbarcode" + generateButtonLabel={} + generator="users_patronBarcode" + modalProps={{ + label: + }} + /> + + } diff --git a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js index bc5071382..739c7ab1e 100644 --- a/src/components/EditSections/EditUserInfo/EditUserInfo.test.js +++ b/src/components/EditSections/EditUserInfo/EditUserInfo.test.js @@ -37,6 +37,10 @@ jest.mock('../../util', () => ({ isConsortiumEnabled: jest.fn(() => true), })); +jest.mock('@folio/service-interaction', () => ({ + NumberGeneratorModalButton: () =>
NumberGeneratorModalButton
+})); + jest.mock('./components', () => ({ EditUserProfilePicture : jest.fn(() => 'Profile Picture'), ChangeUserTypeModal: jest.fn(({ onChange, initialUserType, open }) => { diff --git a/src/routes/UserRecordContainer.js b/src/routes/UserRecordContainer.js index aaee34aef..7230c3b0d 100644 --- a/src/routes/UserRecordContainer.js +++ b/src/routes/UserRecordContainer.js @@ -160,6 +160,11 @@ class UserRecordContainer extends React.Component { type: 'okapi', path: 'users/configurations/entry', }, + configSettings: { + type: 'okapi', + records: 'configs', + path: 'configurations/entries?query=(module==USERS and configName==number_generator)', + }, requestPreferences: { type: 'okapi', throwErrors: false, @@ -215,6 +220,9 @@ class UserRecordContainer extends React.Component { settings: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object), }), + configSettings: PropTypes.shape({ + records: PropTypes.arrayOf(PropTypes.object), + }), loansHistory: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object), }), diff --git a/src/settings/NumberGeneratorOptions.css b/src/settings/NumberGeneratorOptions.css new file mode 100644 index 000000000..78ddf8414 --- /dev/null +++ b/src/settings/NumberGeneratorOptions.css @@ -0,0 +1,5 @@ +@import '@folio/stripes-components/lib/variables'; + +.marginBottomGutter { + margin-bottom: var(--gutter); +} diff --git a/src/settings/NumberGeneratorOptions.js b/src/settings/NumberGeneratorOptions.js new file mode 100644 index 000000000..2ecb10f0e --- /dev/null +++ b/src/settings/NumberGeneratorOptions.js @@ -0,0 +1,100 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { Field } from 'react-final-form'; + +import { stripesConnect, withStripes } from '@folio/stripes/core'; +import { ConfigManager } from '@folio/stripes/smart-components'; + +import { Col, MessageBanner, RadioButton, Row } from '@folio/stripes/components'; + +import css from './NumberGeneratorOptions.css'; + +class NumberGeneratorOptions extends React.Component { + static propTypes = { + stripes: PropTypes.object, + }; + + constructor(props) { + super(props); + this.connectedConfigManager = stripesConnect(ConfigManager); + } + + defaultValues = { + barcodeGeneratorSetting: 'useTextField' + }; + + beforeSave(data) { + return JSON.stringify(data); + } + + getInitialValues = (settings) => { + let loadedValues = {}; + try { + const value = settings.length === 0 ? '' : settings[0].value; + loadedValues = JSON.parse(value); + } catch (e) { + // Make sure we return _something_ because ConfigManager no longer has a safety check here + return {}; + } + + return { + ...this.defaultValues, + ...loadedValues, + }; + } + + render() { + return ( + } + moduleName="USERS" + onBeforeSave={this.beforeSave} + stripes={this.props.stripes} + formType="final-form" + > + + +
+ + + +
+ +
+ + + } + type="radio" + value="useTextField" + /> + } + type="radio" + value="useBoth" + /> + } + type="radio" + value="useGenerator" + /> + + +
+ ); + } +} + +export default withStripes(NumberGeneratorOptions); diff --git a/src/settings/NumberGeneratorOptions.test.js b/src/settings/NumberGeneratorOptions.test.js new file mode 100644 index 000000000..316d4e746 --- /dev/null +++ b/src/settings/NumberGeneratorOptions.test.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { screen } from '@folio/jest-config-stripes/testing-library/react'; + +import '__mock__/stripesCore.mock'; +import '__mock__/stripesSmartComponent.mock'; +import '__mock__/intl.mock'; + +import renderWithRouter from 'helpers/renderWithRouter'; +import NumberGeneratorOptions from './NumberGeneratorOptions'; + +jest.unmock('@folio/stripes/components'); + +const renderNumberGeneratorSettings = (props) => renderWithRouter(); + +describe('Number generator settings', () => { + it('renders', () => { + renderNumberGeneratorSettings(); + + expect(screen.getByTestId('config-manager')).toBeTruthy(); + }); +}); diff --git a/src/settings/sections.js b/src/settings/sections.js index 3cd850049..a0bff3c2d 100644 --- a/src/settings/sections.js +++ b/src/settings/sections.js @@ -18,6 +18,7 @@ import LimitsSettings from './LimitsSettings'; import DepartmentsSettings from './DepartmentsSettings'; import BlockTemplates from './patronBlocks/BlockTemplates'; import TransferCriteriaSettings from './TransferCriteriaSettings'; +import NumberGeneratorOptions from './NumberGeneratorOptions'; const settingsGeneral = [ { @@ -44,6 +45,13 @@ const settingsGeneral = [ component: DepartmentsSettings, perm: 'ui-users.settings.departments.view' }, + { + route: 'numbergeneratoroptions', + label: , + component: NumberGeneratorOptions, + interface: 'servint', + perm: 'ui-users.settings.numberGenerator.manage' + }, // Profile pictures are currently unsupported in Folio and the existence of this setting has // confused implementers. Commenting out for now as opposed to deleting it so the existing // files and components aren't orphaned. diff --git a/src/views/UserEdit/UserEdit.js b/src/views/UserEdit/UserEdit.js index c10f643f5..89422426a 100644 --- a/src/views/UserEdit/UserEdit.js +++ b/src/views/UserEdit/UserEdit.js @@ -120,6 +120,7 @@ class UserEdit extends React.Component { 'addressTypes', 'servicePoints', 'departments', + 'configSettings' ); return formData; diff --git a/src/views/UserEdit/UserEdit.test.js b/src/views/UserEdit/UserEdit.test.js index 2bf812f7c..5819345fc 100644 --- a/src/views/UserEdit/UserEdit.test.js +++ b/src/views/UserEdit/UserEdit.test.js @@ -1,5 +1,7 @@ import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; +import { screen } from '@folio/jest-config-stripes/testing-library/react'; + import renderWithRouter from 'helpers/renderWithRouter'; import UserForm from './UserForm'; @@ -73,6 +75,9 @@ describe('UserEdit', () => { servicePoints: { records: [], }, + configSettings: { + records: [], + }, departments: { records: [], }, @@ -152,6 +157,17 @@ describe('UserEdit', () => { permissions: { records: [{ id: '4', group: 'tst', desc: 'description' }], }, + configSettings: { + records: [ + { + id: 'f2b84177-d85a-4b0c-93dd-279e8f9d1d42', + module: 'USERS', + configName: 'number_generator', + enabled: true, + value: '{"barcodeGeneratorSetting":"useBoth"}', + } + ], + } }, }; const { container } = renderWithRouter(); @@ -184,6 +200,17 @@ describe('UserEdit', () => { permissions: { records: [{ id: '4', group: 'tst', desc: 'description' }], }, + configSettings: { + records: [ + { + id: 'f2b84177-d85a-4b0c-93dd-279e8f9d1d42', + module: 'USERS', + configName: 'number_generator', + enabled: true, + value: '{"barcodeGeneratorSetting":"useBoth"}', + } + ], + } }, }; const { container } = renderWithRouter(); @@ -214,6 +241,17 @@ describe('UserEdit', () => { permissions: { records: [{ id: '4', group: 'tst', desc: 'description' }], }, + configSettings: { + records: [ + { + id: 'f2b84177-d85a-4b0c-93dd-279e8f9d1d42', + module: 'USERS', + configName: 'number_generator', + enabled: true, + value: '{"barcodeGeneratorSetting":"useBoth"}', + } + ], + } }, }; const { container } = renderWithRouter(); @@ -258,6 +296,17 @@ describe('UserEdit', () => { permissions: { records: [{ id: '4', group: 'tst', desc: 'description' }], }, + configSettings: { + records: [ + { + id: 'f2b84177-d85a-4b0c-93dd-279e8f9d1d42', + module: 'USERS', + configName: 'number_generator', + enabled: true, + value: '{"barcodeGeneratorSetting":"useBoth"}', + } + ], + } }, }; const { container } = renderWithRouter(); @@ -288,6 +337,17 @@ describe('UserEdit', () => { permissions: { records: [{ id: '4', group: 'tst', desc: 'description' }], }, + configSettings: { + records: [ + { + id: 'f2b84177-d85a-4b0c-93dd-279e8f9d1d42', + module: 'USERS', + configName: 'number_generator', + enabled: true, + value: '{"barcodeGeneratorSetting":"useBoth"}', + } + ], + } }, }; const { container } = renderWithRouter(); @@ -318,6 +378,17 @@ describe('UserEdit', () => { permissions: { records: [{ id: '4', group: 'tst', desc: 'description' }], }, + configSettings: { + records: [ + { + id: 'f2b84177-d85a-4b0c-93dd-279e8f9d1d42', + module: 'USERS', + configName: 'number_generator', + enabled: true, + value: '{"barcodeGeneratorSetting":"useBoth"}', + } + ], + } }, }; const { container } = renderWithRouter(); @@ -424,6 +495,9 @@ describe('UserEdit', () => { departments: { records: [], }, + configSettings: { + records: [], + }, }, history: { push: jest.fn(), diff --git a/src/views/UserEdit/UserForm.js b/src/views/UserEdit/UserForm.js index bdfba2a2f..1936541ba 100644 --- a/src/views/UserEdit/UserForm.js +++ b/src/views/UserEdit/UserForm.js @@ -373,6 +373,7 @@ class UserForm extends React.Component { accordionId="editUserInfo" initialValues={initialValues} patronGroups={formData.patronGroups} + settings={formData.configSettings} stripes={stripes} form={form} selectedPatronGroup={selectedPatronGroup} diff --git a/test/jest/__mock__/stripesSmartComponent.mock.js b/test/jest/__mock__/stripesSmartComponent.mock.js index 0d2f32ca0..502dce569 100644 --- a/test/jest/__mock__/stripesSmartComponent.mock.js +++ b/test/jest/__mock__/stripesSmartComponent.mock.js @@ -47,6 +47,7 @@ jest.mock('@folio/stripes/smart-components', () => { ...jest.requireActual('@folio/stripes/smart-components'), AddressEditList: () =>
AddressEditList
, ChangeDueDateDialog: (props) =>
ChangeDueDateDialog
, + ConfigManager: () =>
ConfigManager
, ControlledVocab: jest.fn(({ validate }) =>
ControlledVocab
), DueDatePicker: () =>
DueDatePicker
, LocationLookup: () =>
LocationLookup
, diff --git a/translations/ui-users/en.json b/translations/ui-users/en.json index f719545bf..84a9f566b 100644 --- a/translations/ui-users/en.json +++ b/translations/ui-users/en.json @@ -167,6 +167,13 @@ "settings.departments.users": "Users", "settings.manualBlockTemplates": "Templates", "settings.manualBlockTemplates.paneTitle": "Patron block templates", + "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.useGeneratorForBarcode": "Number generator on, fixed: the barcode can be filled using the generator only.", + "settings.numberGeneratorOptions.useTextFieldForBarcode": "Number generator off: the barcode can be filled manually only.", + "settings.numberGeneratorOptions.useBothForBarcode": "Number generator on, editable: the barcode can be filled using the generator and be edited, or filled manually.", + "numberGenerator.generateUserBarcode": "Generate user barcode", + "numberGenerator.userBarcodeGenerator": "User barcode generator", "manualBlockTemplates.templateInformation": "Template information", "manualBlockTemplates.templateName": "Template name", "manualBlockTemplates.blockCode": "Block code",