From 2345a2d9d41707a2ccdeca4b36f99985cb1cf8a3 Mon Sep 17 00:00:00 2001 From: Vadym Shchekotilin <86330150+vashjs@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:09:00 +0100 Subject: [PATCH] UIBULKED-562 Include statistical code option on Instances bulk edit forms (#663) --- CHANGELOG.md | 1 + package.json | 4 +- .../ContentUpdatesForm/ValuesColumn.js | 9 ++ .../ContentUpdatesForm/ValuesColumn.test.js | 48 ++++++--- .../InstanceStatisticalCodesControl.js | 51 ++++++++++ .../ContentUpdatesForm/helpers.js | 25 ++++- .../ContentUpdatesForm/helpers.test.js | 44 +++++++++ src/constants/core.js | 1 + src/constants/inAppActions.js | 15 +++ src/constants/selectOptions.js | 12 +++ src/hooks/api/index.js | 1 + src/hooks/api/useStatisticalCode.test.js | 98 +++++++++++++++++++ src/hooks/api/useStatisticalCodes.js | 41 ++++++++ src/utils/helpers.js | 15 +++ src/utils/helpers.test.js | 74 ++++++++++++++ translations/ui-bulk-edit/en.json | 4 + 16 files changed, 429 insertions(+), 14 deletions(-) create mode 100644 src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/controls/InstanceStatisticalCodesControl.js create mode 100644 src/hooks/api/useStatisticalCode.test.js create mode 100644 src/hooks/api/useStatisticalCodes.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b94cd76..c0ab2c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * [UIBULKED-560](https://folio-org.atlassian.net/browse/UIBULKED-560) Update actions menu. * [UIBULKED-585](https://folio-org.atlassian.net/browse/UUIBULKED-585) Adding missing translation and filters. * [UIBULKED-561](https://folio-org.atlassian.net/browse/UUIBULKED-561) Add administrative data accordion to MARC bulk edit form. +* [UIBULKED-562](https://folio-org.atlassian.net/browse/UIBULKED-562) Include statistical code option on Instances bulk edit forms. ## [4.2.2](https://github.com/folio-org/ui-bulk-edit/tree/v4.2.2) (2024-11-15) diff --git a/package.json b/package.json index f7c37e7f..bddf384f 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,9 @@ "consortia.publications-results.item.get", "consortium-search.institutions.collection.get", "consortium-search.campuses.collection.get", - "consortium-search.libraries.collection.get" + "consortium-search.libraries.collection.get", + "inventory-storage.statistical-codes.collection.get", + "inventory-storage.statistical-code-types.collection.get" ] }, { diff --git a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.js b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.js index b81c7b95..e7399dbd 100644 --- a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.js +++ b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.js @@ -27,6 +27,7 @@ import { InstanceNotesControl } from './controls/InstanceNotesControl'; import { ElectronicAccessRelationshipControl } from './controls/ElectronicAccessRelationshipControl'; import { DuplicateNoteControl } from './controls/DuplicateNotesControl'; import { StatusControl } from './controls/StatusControl'; +import { InstanceStatisticalCodesControl } from './controls/InstanceStatisticalCodesControl'; export const ValuesColumn = ({ action, allActions, actionIndex, onChange, option }) => { @@ -150,6 +151,13 @@ export const ValuesColumn = ({ action, allActions, actionIndex, onChange, option /> ); + const renderStatisticalCodesSelect = () => controlType === CONTROL_TYPES.STATISTICAL_CODES_SELECT && ( + <InstanceStatisticalCodesControl + actionName={action.name} + {...sharedProps} + /> + ); + return ( <> {renderTextField()} @@ -162,6 +170,7 @@ export const ValuesColumn = ({ action, allActions, actionIndex, onChange, option {renderNoteTypeSelect()} {renderNoteDuplicateTypeSelect()} {renderElectronicAccessRelationshipSelect()} + {renderStatisticalCodesSelect()} </> ); }; diff --git a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.test.js b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.test.js index c0847241..a5dc00cc 100644 --- a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.test.js +++ b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/ValuesColumn.test.js @@ -9,25 +9,33 @@ import { createMemoryHistory } from 'history'; import { runAxeTest } from '@folio/stripes-testing'; import { queryClient } from '../../../../../../test/jest/utils/queryClient'; import { ValuesColumn } from './ValuesColumn'; -import { useElectronicAccessRelationships, useLoanTypes, usePatronGroup } from '../../../../../hooks/api'; +import { + useElectronicAccessRelationships, + useLoanTypes, + usePatronGroup, + useStatisticalCodes +} from '../../../../../hooks/api'; import { CAPABILITIES, CONTROL_TYPES } from '../../../../../constants'; + jest.mock('../../../../../hooks/api/useLoanTypes'); jest.mock('../../../../../hooks/api/usePatronGroup'); jest.mock('../../../../../hooks/api/useElectronicAccess'); +jest.mock('../../../../../hooks/api/useStatisticalCodes'); const onChange = jest.fn(); const history = createMemoryHistory(); -const mockAction = { - type: '', - name: 'testName', - value: 'testValue', -}; +const renderComponent = (actionType, override = {}) => { + const action = { + type: '', + name: 'testName', + value: 'testValue', + controlType: actionType, + ...override, + }; -const renderComponent = (actionType) => { - const action = { ...mockAction, controlType: actionType }; return render( <Router history={history}> <QueryClientProvider client={queryClient}> @@ -54,11 +62,11 @@ describe('ValuesColumn Component', () => { isElectronicAccessLoading: false, electronicAccessRelationships: [], }); - }); - afterEach(() => { - usePatronGroup.mockReset(); - useLoanTypes.mockReset(); + useStatisticalCodes.mockReturnValue({ + statisticalCodes: [], + isStatisticalCodesLoading: false, + }); }); it('should render TextField when action type is INPUT', async () => { @@ -191,6 +199,22 @@ describe('ValuesColumn Component', () => { await waitFor(() => expect(onChange).toHaveBeenCalled()); }); + it('should render select with statistical codes when action type is ADD, or REMOVE_SOME', async () => { + const { getByRole } = renderComponent( + () => CONTROL_TYPES.STATISTICAL_CODES_SELECT, + { + value: [{ label: 'test', value: 'test' }], + } + ); + const element = getByRole('combobox'); + + expect(element).toBeInTheDocument(); + + fireEvent.change(element, [{ label: 'test', value: 'test' }]); + + await waitFor(() => expect(onChange).toHaveBeenCalled()); + }); + it('should render with no axe errors', async () => { renderComponent(() => CONTROL_TYPES.ELECTRONIC_ACCESS_RELATIONSHIP_SELECT); diff --git a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/controls/InstanceStatisticalCodesControl.js b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/controls/InstanceStatisticalCodesControl.js new file mode 100644 index 00000000..954e20f5 --- /dev/null +++ b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/controls/InstanceStatisticalCodesControl.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { Loading, MultiSelection } from '@folio/stripes/components'; + +import { FIELD_VALUE_KEY, getLabelByValue, sortWithoutPlaceholder } from '../helpers'; +import { useStatisticalCodes } from '../../../../../../hooks/api/useStatisticalCodes'; +import { customMultiSelectionFilter } from '../../../../../../utils/helpers'; + + +export const InstanceStatisticalCodesControl = ({ actionName, actionValue, actionIndex, onChange }) => { + const { formatMessage } = useIntl(); + + const { statisticalCodes, isStatisticalCodesLoading } = useStatisticalCodes(); + const sortedStatisticalCodes = sortWithoutPlaceholder(statisticalCodes); + const title = getLabelByValue(sortedStatisticalCodes, actionValue); + + const handleChange = value => { + onChange({ + actionIndex, + value, + fieldName: FIELD_VALUE_KEY, + }); + }; + + if (isStatisticalCodesLoading) return <Loading size="large" />; + + return ( + <div title={title}> + <MultiSelection + key={actionName} + id="statisticalCodes" + value={actionValue} + onChange={handleChange} + placeholder={formatMessage({ id: 'ui-bulk-edit.layer.statisticalCode' })} + aria-label={formatMessage({ id: 'ui-bulk-edit.ariaLabel.statisticalCode' })} + dataOptions={statisticalCodes} + dirty={!!actionValue} + filter={customMultiSelectionFilter} + /> + </div> + ); +}; + +InstanceStatisticalCodesControl.propTypes = { + actionValue: PropTypes.arrayOf(PropTypes.object), + actionName: PropTypes.string, + actionIndex: PropTypes.number, + onChange: PropTypes.func, +}; diff --git a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.js b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.js index be700f78..9dd8a644 100644 --- a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.js +++ b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.js @@ -21,6 +21,7 @@ import { noteActionsWithMarc, noteActionsWithDuplicate, electronicAccess, + statisticalCodeActions, } from '../../../../../constants'; import { getActionParameters } from '../../../../../constants/actionParameters'; @@ -89,6 +90,7 @@ export const getDefaultActions = ({ const expirationDefaultActions = expirationActions(); const holdingsLocationDefaultActions = permanentHoldingsLocation(); const suppressDefaultActions = suppressFromDiscActions(); + const statisticalCodeDefaultActions = statisticalCodeActions(); const statusDefaultActions = statusActions(); const loanDefaultActions = permanentLoanTypeActions(); const noteDefaultActions = noteActions(); @@ -196,6 +198,19 @@ export const getDefaultActions = ({ }, ], }; + case OPTIONS.STATISTICAL_CODE: + return { + type: '', + actions: [ + null, + { + actionsList: statisticalCodeDefaultActions, + controlType: () => CONTROL_TYPES.STATISTICAL_CODES_SELECT, + [ACTION_VALUE_KEY]: statisticalCodeDefaultActions[0].value, + [FIELD_VALUE_KEY]: '', + }, + ], + }; case OPTIONS.STAFF_SUPPRESS: return { type: '', @@ -425,6 +440,8 @@ export const getLabelByValue = (items, targetValue) => { }; export const sortWithoutPlaceholder = (array) => { + if (!array.length) return []; + const [placeholder, ...rest] = array; return [placeholder, ...rest.sort((a, b) => a.label.localeCompare(b.label))]; @@ -433,7 +450,13 @@ export const sortWithoutPlaceholder = (array) => { export const getMappedContentUpdates = (fields, options) => fields.map(({ parameters, tenants, option, actionsDetails: { actions } }) => { - const [initial, updated] = actions.map(action => action?.value ?? null); + const [initial, updated] = actions.map(action => { + if (Array.isArray(action?.value)) { + return action.value.map(item => item?.value).join(','); + } + + return action?.value || null; + }); const actionTenants = actions.map(action => action?.tenants); const sourceOption = options.find(o => o.value === option); const optionType = sourceOption?.type; diff --git a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.test.js b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.test.js index 277e8fd2..ce8fbf5b 100644 --- a/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.test.js +++ b/src/components/BulkEditPane/BulkEditListResult/BulkEditInApp/ContentUpdatesForm/helpers.test.js @@ -879,6 +879,50 @@ describe('ContentUpdatesForm helpers', () => { ); }); + it('returns the correct object for the ELECTRONIC_ACCESS_MATERIALS_SPECIFIED option', () => { + expect(JSON.stringify(getDefaultActions({ + option: OPTIONS.STATISTICAL_CODE, + options: [], + formatMessage, + capability: CAPABILITIES.INSTANCE + }))) + .toEqual( + JSON.stringify({ + type: '', + actions: [ + null, + { + actionsList: [ + { + value: '', + label: <FormattedMessage id="ui-bulk-edit.actions.placeholder" />, + disabled: true, + }, + { + value: ACTIONS.ADD_TO_EXISTING, + label: <FormattedMessage id="ui-bulk-edit.layer.options.add" />, + disabled: false, + }, + { + value: ACTIONS.REMOVE_SOME, + label: <FormattedMessage id="ui-bulk-edit.layer.options.items.removeNote" />, + disabled: false, + }, + { + value: ACTIONS.REMOVE_ALL, + label: <FormattedMessage id="ui-bulk-edit.layer.options.items.removeAll" />, + disabled: false, + }, + ], + controlType: () => CONTROL_TYPES.STATISTICAL_CODES_SELECT, + [ACTION_VALUE_KEY]: '', + [FIELD_VALUE_KEY]: '', + }, + ], + }), + ); + }); + it('returns the correct object for the default case', () => { expect(getDefaultActions({ option: 'unknown', diff --git a/src/constants/core.js b/src/constants/core.js index a9c05874..e57e0ffd 100644 --- a/src/constants/core.js +++ b/src/constants/core.js @@ -94,6 +94,7 @@ export const CONTROL_TYPES = { NOTE_SELECT: 'NOTE_SELECT', NOTE_DUPLICATE_SELECT: 'NOTE_DUPLICATE_SELECT', ELECTRONIC_ACCESS_RELATIONSHIP_SELECT: 'ELECTRONIC_ACCESS_RELATIONSHIP_SELECT', + STATISTICAL_CODES_SELECT: 'STATISTICAL_CODES_SELECT', }; export const TRANSLATION_SUFFIX = { diff --git a/src/constants/inAppActions.js b/src/constants/inAppActions.js index cdb953c3..18d7c32b 100644 --- a/src/constants/inAppActions.js +++ b/src/constants/inAppActions.js @@ -12,6 +12,7 @@ export const ACTIONS = { MARK_AS_STAFF_ONLY: 'MARK_AS_STAFF_ONLY', REMOVE_MARK_AS_STAFF_ONLY: 'REMOVE_MARK_AS_STAFF_ONLY', REMOVE_ALL: 'REMOVE_ALL', + REMOVE_SOME: 'REMOVE_SOME', CHANGE_TYPE: 'CHANGE_TYPE', DUPLICATE: 'DUPLICATE', @@ -119,6 +120,12 @@ export const getRemoveTheseAction = () => ({ disabled: false, }); +export const getRemoveSomeAction = () => ({ + value: ACTIONS.REMOVE_SOME, + label: <FormattedMessage id="ui-bulk-edit.layer.options.items.removeNote" />, + disabled: false, +}); + export const getDuplicateToNoteAction = () => ({ value: ACTIONS.DUPLICATE, label: <FormattedMessage id="ui-bulk-edit.layer.options.items.duplicateTo" />, @@ -142,6 +149,14 @@ export const suppressFromDiscActions = () => [ getSetToTrueAction(), getSetToFalseAction(), ]; + +export const statisticalCodeActions = () => [ + getPlaceholder(), + getAddAction(), + getRemoveSomeAction(), + getRemoveAllAction(), +]; + export const noteActions = () => [ getPlaceholder(), getAddToExistingAction(), diff --git a/src/constants/selectOptions.js b/src/constants/selectOptions.js index 772ea434..9af1c767 100644 --- a/src/constants/selectOptions.js +++ b/src/constants/selectOptions.js @@ -10,6 +10,7 @@ export const OPTIONS = { TEMPORARY_LOCATION: 'TEMPORARY_LOCATION', PERMANENT_LOCATION: 'PERMANENT_LOCATION', SUPPRESS_FROM_DISCOVERY: 'SUPPRESS_FROM_DISCOVERY', + STATISTICAL_CODE: 'STATISTICAL_CODE', STAFF_SUPPRESS: 'STAFF_SUPPRESS', STATUS: 'STATUS', EXPIRATION_DATE: 'EXPIRATION_DATE', @@ -287,6 +288,12 @@ export const getInstanceOptions = (formatMessage, instanceNotes) => [ disabled: false, categoryName: formatMessage({ id: 'ui-bulk-edit.category.administrativeData' }), }, + { + value: OPTIONS.STATISTICAL_CODE, + label: formatMessage({ id: 'ui-bulk-edit.layer.options.instances.statisticalCode' }), + disabled: false, + categoryName: formatMessage({ id: 'ui-bulk-edit.category.administrativeData' }), + }, ...instanceNotes ]; @@ -306,6 +313,11 @@ export const getAdministrativeDataOptions = (formatMessage) => [ label: formatMessage({ id: 'ui-bulk-edit.layer.options.instances.suppress' }), disabled: false, }, + { + value: OPTIONS.STATISTICAL_CODE, + label: formatMessage({ id: 'ui-bulk-edit.layer.options.instances.statisticalCode' }), + disabled: false, + }, ]; export const getHoldingsNotes = (formatMessage, holdingsNotes) => [ diff --git a/src/hooks/api/index.js b/src/hooks/api/index.js index 81dda79f..cc770080 100644 --- a/src/hooks/api/index.js +++ b/src/hooks/api/index.js @@ -20,3 +20,4 @@ export * from './useHoldingsNotesEcs'; export * from './useLocationEcs'; export * from './useLoanTypesEcs'; export * from './useElectronicAccessEcs'; +export * from './useStatisticalCodes'; diff --git a/src/hooks/api/useStatisticalCode.test.js b/src/hooks/api/useStatisticalCode.test.js new file mode 100644 index 00000000..a8539705 --- /dev/null +++ b/src/hooks/api/useStatisticalCode.test.js @@ -0,0 +1,98 @@ +import { QueryClient, QueryClientProvider } from 'react-query'; +import { renderHook } from '@testing-library/react-hooks'; + +import { useNamespace, useOkapiKy } from '@folio/stripes/core'; + +import { useErrorMessages } from '../useErrorMessages'; +import { useStatisticalCodes, STATISTICAL_CODES_KEY } from './useStatisticalCodes'; +import { getMappedStatisticalCodes } from '../../utils/helpers'; + + +jest.mock('@folio/stripes/core', () => ({ + useNamespace: jest.fn(), + useOkapiKy: jest.fn(), +})); + +jest.mock('../useErrorMessages', () => ({ + useErrorMessages: jest.fn(), +})); + +jest.mock('../../utils/helpers', () => ({ + getMappedStatisticalCodes: jest.fn(), +})); + +describe('useStatisticalCodes', () => { + const queryClient = new QueryClient(); + const wrapper = ({ children }) => ( + <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> + ); + + const mockNamespaceKey = `${STATISTICAL_CODES_KEY}_namespace`; + const mockKy = { + get: jest.fn(), + }; + const mockShowErrorMessage = jest.fn(); + const mockMappedStatisticalCodes = [{ id: '1', name: 'Code 1' }]; + + beforeEach(() => { + jest.clearAllMocks(); + + useNamespace.mockReturnValue([mockNamespaceKey]); + useOkapiKy.mockReturnValue(mockKy); + useErrorMessages.mockReturnValue({ showErrorMessage: mockShowErrorMessage }); + getMappedStatisticalCodes.mockReturnValue(mockMappedStatisticalCodes); + }); + + it('should fetch and return statistical codes data', async () => { + const statisticalCodeTypes = [{ id: 'type1', name: 'Type 1' }]; + const statisticalCodes = [{ id: 'code1', name: 'Code 1' }]; + + mockKy.get.mockImplementation((url) => { + if (url === 'statistical-code-types') { + return { + json: async () => ({ statisticalCodeTypes }), + }; + } + + if (url === 'statistical-codes') { + return { + json: async () => ({ statisticalCodes }), + }; + } + + throw new Error('Unexpected URL'); + }); + + const { result, waitFor } = renderHook(() => useStatisticalCodes(), { wrapper }); + + await waitFor(() => !result.current.isStatisticalCodesLoading); + + expect(result.current.statisticalCodes).toEqual(mockMappedStatisticalCodes); + expect(result.current.isStatisticalCodesLoading).toBe(false); + expect(mockKy.get).toHaveBeenCalledWith('statistical-code-types', { + searchParams: { query: 'cql.allRecords=1', limit: 1000 }, + }); + expect(mockKy.get).toHaveBeenCalledWith('statistical-codes', { + searchParams: { query: 'cql.allRecords=1', limit: 1000 }, + }); + expect(getMappedStatisticalCodes).toHaveBeenCalledWith([ + statisticalCodeTypes, + statisticalCodes, + ]); + }); + + it('should handle errors and call showErrorMessage', async () => { + const error = new Error('Failed to fetch'); + mockKy.get.mockRejectedValue(error); + + const { result, waitFor } = renderHook(() => useStatisticalCodes(), { wrapper }); + + await waitFor(() => !result.current.isStatisticalCodesLoading); + + expect(result.current.statisticalCodes).toEqual([{ + id: '1', + name: 'Code 1', + }]); + expect(result.current.isStatisticalCodesLoading).toBe(false); + }); +}); diff --git a/src/hooks/api/useStatisticalCodes.js b/src/hooks/api/useStatisticalCodes.js new file mode 100644 index 00000000..19e8cce5 --- /dev/null +++ b/src/hooks/api/useStatisticalCodes.js @@ -0,0 +1,41 @@ +import { useQuery } from 'react-query'; + +import { useNamespace, useOkapiKy } from '@folio/stripes/core'; + +import { useErrorMessages } from '../useErrorMessages'; +import { getMappedStatisticalCodes } from '../../utils/helpers'; + + +export const STATISTICAL_CODES_KEY = 'STATISTICAL_CODES_KEY'; + +export const useStatisticalCodes = (options = {}) => { + const ky = useOkapiKy(); + const [namespaceKey] = useNamespace({ key: STATISTICAL_CODES_KEY }); + const { showErrorMessage } = useErrorMessages(); + + const sharedParams = { searchParams: { query: 'cql.allRecords=1', limit: 1000 } }; + + const { data, isLoading: isStatisticalCodesLoading } = useQuery( + { + queryKey: [namespaceKey], + cacheTime: Infinity, + staleTime: Infinity, + queryFn: () => Promise.all([ + ky.get('statistical-code-types', sharedParams).json() + .then(response => response.statisticalCodeTypes), + ky.get('statistical-codes', sharedParams).json() + .then(response => response.statisticalCodes), + ]), + select: getMappedStatisticalCodes, + onError: showErrorMessage, + ...options, + }, + ); + + const statisticalCodes = data || []; + + return { + statisticalCodes, + isStatisticalCodesLoading, + }; +}; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 6f988857..fc50bf25 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -110,6 +110,10 @@ export const customFilter = (value, dataOptions) => { }, []); }; +export const customMultiSelectionFilter = (value, dataOptions) => { + return { renderedItems: customFilter(value, dataOptions) }; +}; + export const setIn = (obj, path, value) => { return setWith(clone(obj), path, value, clone); }; @@ -176,3 +180,14 @@ export const getTransformedLogsFilterValue = (values) => { return Array.from(result); }; + +export const getMappedStatisticalCodes = ([statisticalCodeTypes, statisticalCodesArr]) => { + return statisticalCodesArr.map((statisticalCode) => { + const type = statisticalCodeTypes.find((codeType) => codeType.id === statisticalCode.statisticalCodeTypeId); + + return { + label: `${type.name}: ${statisticalCode.code} - ${statisticalCode.name}`, + value: statisticalCode.id, + }; + }); +}; diff --git a/src/utils/helpers.test.js b/src/utils/helpers.test.js index ec94aa94..fbd91dd8 100644 --- a/src/utils/helpers.test.js +++ b/src/utils/helpers.test.js @@ -1,5 +1,6 @@ import { customFilter, + getMappedStatisticalCodes, getTenantsById, getTransformedLogsFilterValue, removeDuplicatesByValue @@ -280,3 +281,76 @@ describe('getTransformedLogsFilterValue', () => { expect(result).toContain('other_value'); }); }); + +describe('getMappedStatisticalCodes', () => { + test('should correctly map statistical codes with corresponding types', () => { + const statisticalCodeTypes = [ + { id: '1', name: 'Type A' }, + { id: '2', name: 'Type B' }, + ]; + + const statisticalCodesArr = [ + { id: 'code1', statisticalCodeTypeId: '1', code: '001', name: 'Code One' }, + { id: 'code2', statisticalCodeTypeId: '2', code: '002', name: 'Code Two' }, + ]; + + const expected = [ + { label: 'Type A: 001 - Code One', value: 'code1' }, + { label: 'Type B: 002 - Code Two', value: 'code2' }, + ]; + + const result = getMappedStatisticalCodes([statisticalCodeTypes, statisticalCodesArr]); + expect(result).toEqual(expected); + }); + + test('should return an empty array when statisticalCodesArr is empty', () => { + const statisticalCodeTypes = [ + { id: '1', name: 'Type A' }, + { id: '2', name: 'Type B' }, + ]; + + const statisticalCodesArr = []; + + const expected = []; + + const result = getMappedStatisticalCodes([statisticalCodeTypes, statisticalCodesArr]); + expect(result).toEqual(expected); + }); + + test('should throw an error when a statistical code has no matching type', () => { + const statisticalCodeTypes = [ + { id: '1', name: 'Type A' }, + ]; + + const statisticalCodesArr = [ + { id: 'code1', statisticalCodeTypeId: '2', code: '002', name: 'Code Two' }, + ]; + + expect(() => { + getMappedStatisticalCodes([statisticalCodeTypes, statisticalCodesArr]); + }).toThrow(TypeError); + }); + + test('should correctly map multiple statistical codes', () => { + const statisticalCodeTypes = [ + { id: '1', name: 'Type A' }, + { id: '2', name: 'Type B' }, + { id: '3', name: 'Type C' }, + ]; + + const statisticalCodesArr = [ + { id: 'code1', statisticalCodeTypeId: '1', code: '001', name: 'Code One' }, + { id: 'code2', statisticalCodeTypeId: '2', code: '002', name: 'Code Two' }, + { id: 'code3', statisticalCodeTypeId: '3', code: '003', name: 'Code Three' }, + ]; + + const expected = [ + { label: 'Type A: 001 - Code One', value: 'code1' }, + { label: 'Type B: 002 - Code Two', value: 'code2' }, + { label: 'Type C: 003 - Code Three', value: 'code3' }, + ]; + + const result = getMappedStatisticalCodes([statisticalCodeTypes, statisticalCodesArr]); + expect(result).toEqual(expected); + }); +}); diff --git a/translations/ui-bulk-edit/en.json b/translations/ui-bulk-edit/en.json index a6fbfcd7..1d8c16bd 100644 --- a/translations/ui-bulk-edit/en.json +++ b/translations/ui-bulk-edit/en.json @@ -326,6 +326,7 @@ "columns.INSTANCE.Publication frequency": "Publication frequency", "columns.INSTANCE.Publication range": "Publication range", "columns.INSTANCE.Notes" : "Notes", + "columns.INSTANCE.Statistical code" : "Statistical code", "columns.logs.hrId": "ID", "columns.logs.operationType": "Bulk operation type", @@ -384,6 +385,7 @@ "layer.options.holdings.urlPublic": "URL public note", "layer.options.instances.staffSuppress": "Staff suppress", "layer.options.instances.suppress": "Suppress from discovery", + "layer.options.instances.statisticalCode": "Statistical code", "layer.options.expirationDate": "Expiration date", "layer.options.email": "Email", "layer.options.patronGroup": "Patron group", @@ -409,6 +411,7 @@ "layer.selectLocation": "Select location", "layer.selectPatronGroup": "Select patron group", "layer.selectLoanType": "Select loan type", + "layer.statisticalCode": "Select statistical code", "layer.selectNoteType": "Select note type", "layer.selectType": "Select note type", "layer.options.items.true": "Set true", @@ -517,6 +520,7 @@ "ariaLabel.location": "Location", "ariaLabel.statusSelect": "Status select", "ariaLabel.loanTypeSelect": "Loan type select", + "ariaLabel.statisticalCode": "Statistical code select", "ariaLabel.urlRelationshipSelect": "Url relationship select", "ariaLabel.columnFilter": "Column filter input",