From c42845afc317c8b6805e02ecde367a849662a09f Mon Sep 17 00:00:00 2001 From: ssandupatla <58558770+ssandupatla@users.noreply.github.com> Date: Fri, 28 Apr 2023 16:38:11 +0530 Subject: [PATCH 01/68] UIIN-1584 create JEST/RTL test cases for useFacets.js (#2136) --- src/common/hooks/useFacets.test.js | 312 +++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 src/common/hooks/useFacets.test.js diff --git a/src/common/hooks/useFacets.test.js b/src/common/hooks/useFacets.test.js new file mode 100644 index 000000000..5791ee5f9 --- /dev/null +++ b/src/common/hooks/useFacets.test.js @@ -0,0 +1,312 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useLocation } from 'react-router-dom'; +import React from 'react'; +import { useFacetSettings } from '../../stores/facetsStore'; +import useFacets from './useFacets'; + +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn(), +})); + +jest.mock('../../stores/facetsStore', () => ({ + useFacetSettings: jest + .fn() + .mockReturnValue([{ foo: { value: 'bar' }, quux: { value: 'changed' } }]), +})); + +describe('useFacets', () => { + const segmentAccordions = { + test: true, + foo: true, + baz: false, + }; + const segmentOptions = { baz: ['qux'], quux: ['corge'] }; + const selectedFacetFilters = { + selectedFilters: { foo: { value: 'bar' } }, + facetName: 'test', + }; + const getNewRecords = jest.fn(() => { + return { quux: ['corge', 'grault'], garply: ['waldo'] }; + }); + + const data = { + query: { query: null, filters: 'filters' }, + onFetchFacets: jest.fn(), + parentResources: { + facets: { records: [[{ name: 'baz' }]], isPending: true }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + useLocation.mockReturnValue({ pathname: '/path' }); + useFacetSettings.mockReturnValue([selectedFacetFilters, jest.fn()]); + }); + + it('returns initial state', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + expect(result.current[0]).toEqual(segmentAccordions); + }); + + it('updates accordions state on new toggle', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + + act(() => { + result.current[1]({ id: 'foo' }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: false, + }); + }); + + it('updates accordions state on open toggle', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + + act(() => { + result.current[2]({ + onMoreClickedFacet: true, + focusedFacet: '', + facetToOpen: true, + dateFacet: true, + }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + + it('updates accordions state on segment toggle', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + {}, + getNewRecords, + data + )); + + act(() => { + result.current[2]({ + onMoreClickedFacet: true, + focusedFacet: '', + facetToOpen: true, + dateFacet: true, + }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + + it('updates accordions state on records toggle', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + + act(() => { + result.current[2]({ + onMoreClickedFacet: true, + focusedFacet: '', + facetToOpen: false, + dateFacet: true, + }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + + it('updates accordions state on facet toggle', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + + act(() => { + result.current[2]({ + onMoreClickedFacet: false, + focusedFacet: true, + facetToOpen: false, + dateFacet: true, + }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + + it('updates accordions state on date toggle', () => { + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + + act(() => { + result.current[2]({ + onMoreClickedFacet: false, + focusedFacet: true, + facetToOpen: false, + dateFacet: false, + }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + + it('updates facet settings on data filter search', () => { + const spy = jest.spyOn(React, 'useRef'); + spy.mockImplementation(() => { + return { current: {}, ...segmentAccordions }; + }); + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + selectedFacetFilters, + getNewRecords, + data + )); + + act(() => { + result.current[3]({ name: 'quux', value: 'changed' }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + + it('updates facet settings on accordions filter search', () => { + const spy = jest.spyOn(React, 'useRef'); + spy.mockImplementation(() => { + return { current: { test: ['ere'] }, ...segmentAccordions }; + }); + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + { selectedFilters: undefined, facetName: 'test' }, + getNewRecords, + data + )); + act(() => { + result.current[3]({ name: 'quux', value: 'changed' }); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + it('updates facet settings on selected filter search', () => { + jest.spyOn(React, 'useRef').mockReturnValueOnce({ + current: {}, + }); + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + { + selectedFilters: undefined, + facetName: 'test', + }, + getNewRecords, + data + )); + + act(() => { + result.current[5]('test'); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + it('updates facet settings on facet filter search', () => { + const spy = jest.spyOn(React, 'useRef'); + spy.mockImplementation(() => { + return { + current: { + test: ['ere'], + prevFacetValue: 'test', + selectedFilters: ['hggh'], + facetName: 'test', + }, + ...segmentAccordions, + }; + }); + const { result } = renderHook(() => useFacets( + segmentAccordions, + segmentOptions, + { + selectedFilters: undefined, + facetName: 'test', + }, + getNewRecords, + data + )); + + act(() => { + result.current[5]('test'); + }); + expect(result.current[0]).toEqual({ + ...segmentAccordions, + foo: true, + }); + }); + it('updates facet settings on ref filter search', () => { + const spy = jest.spyOn(React, 'useRef'); + spy.mockImplementation(() => { + return { + current: { test: ['ere'], prevFacetValue: 'test', facetName: 'test' }, + ...segmentAccordions, + }; + }); + const { result } = renderHook(() => useFacets( + { foo: false }, + segmentOptions, + { + selectedFilters: undefined, + facetName: 'test', + }, + getNewRecords, + data + )); + act(() => { + result.current[5]('test'); + }); + expect(result.current[0]).toEqual({ + foo: false, + }); + }); +}); From fb26f7dd6a2be4861185a4b8709f9e2e41570370 Mon Sep 17 00:00:00 2001 From: cchitneedi <100114810+cchitneedi@users.noreply.github.com> Date: Fri, 28 Apr 2023 17:10:27 +0530 Subject: [PATCH 02/68] UIIN-1585 create Jest/RTL test for holdingsRecordFilterRenderer.js (#2133) * UIIN-1585 create Jest/RTL test for holdingsRecordFilterRenderer.js * Update holdingsRecordFilterRenderer.test.js removed unused import --- .../holdingsRecordFilterRenderer.test.js | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/components/HoldingsRecordFilters/holdingsRecordFilterRenderer.test.js diff --git a/src/components/HoldingsRecordFilters/holdingsRecordFilterRenderer.test.js b/src/components/HoldingsRecordFilters/holdingsRecordFilterRenderer.test.js new file mode 100644 index 000000000..4a461b3f9 --- /dev/null +++ b/src/components/HoldingsRecordFilters/holdingsRecordFilterRenderer.test.js @@ -0,0 +1,118 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; +import '../../../test/jest/__mock__/currencyData.mock'; +import '../../../test/jest/__mock__/stripesConfig.mock'; +import '../../../test/jest/__mock__/stripesCore.mock'; +import '../../../test/jest/__mock__/stripesIcon.mock'; +import renderWithIntl from '../../../test/jest/helpers/renderWithIntl'; +import translationsProperties from '../../../test/jest/helpers/translationsProperties'; + +import holdingsRecordFilterRenderer from './holdingsRecordFilterRenderer'; + + +import { + getSourceOptions, + getSuppressedOptions, + processFacetOptions, + processStatisticalCodes, +} from '../../facetUtils'; + +jest.mock('../../facetUtils', () => ({ + ...jest.requireActual('../../facetUtils'), + getSourceOptions: jest.fn(), + getSuppressedOptions: jest.fn(), + processFacetOptions: jest.fn(), + processStatisticalCodes: jest.fn(), +})); + +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), + Accordion: jest.fn(({ onClearFilter, children }) => ( +
+ {children} + +
+ )), +}), { virtual: true }); + +jest.mock('../CheckboxFacet/CheckboxFacet', () => ({ onChange }) => ( +
+
CheckboxFacet
+ +
+)); + +const onChangeMock = jest.fn(); +const resources = { + facets: { + hasLoaded: true, + resource: 'facets', + records: [{ + 'items.effectiveLocationId': 'effectiveLocationId1', + 'holdings.permanentLocationId': 'permanentLocationId1', + 'holdings.statisticalCodeIds': 'statisticalCodeIds1', + 'holdings.discoverySuppress': 'discoverySuppress1', + 'holdings.sourceId': 'sourceId1', + 'holdingsTags': 'holdingsTags1', + 'holdings.holdingsTypeId': 'holdingsTypeId1', + }], + }, +}; +const DATA = { + + locations: [], + statisticalCodes: [], + holdingsSources: [], + holdingsTypes: [], + tags: [], + query: { filters: '' }, + onFetchFacets: jest.fn(), + parentResources: resources +}; + +const renderFilters = (data = DATA, onChange = onChangeMock) => renderWithIntl( + {holdingsRecordFilterRenderer(data)(onChange)}, + translationsProperties +); + + +describe('holdingsRecordFilterRenderer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('processFacetOptions should be called when holdingsRecordFilterRenderer renders', () => { + renderFilters(); + expect(processFacetOptions).toHaveBeenCalledTimes(4); + }); + + it('getSuppressedOptions should be called when holdingsRecordFilterRenderer renders', () => { + renderFilters(); + expect(getSuppressedOptions).toHaveBeenCalledTimes(1); + }); + it('getSourceOptions should be called when holdingsRecordFilterRenderer renders', () => { + renderFilters(); + expect(getSourceOptions).toBeCalled(); + }); + it('processStatisticalCodes should be called when holdingsRecordFilterRenderer renders', () => { + renderFilters(); + expect(processStatisticalCodes).toHaveBeenCalledTimes(1); + }); + + it('onChange function to be called when clearfilter button is clicked', () => { + renderFilters(); + userEvent.click(screen.getAllByRole('button', { name: 'onClearFilter' })[0]); + expect(onChangeMock).toBeCalled(); + }); + it('onChange function to be called when onChange button is clicked', () => { + renderFilters(); + userEvent.click(screen.getAllByRole('button', { name: 'onChange' })[0]); + expect(onChangeMock).toBeCalled(); + }); +}); + + + From 64a8b0e64366a5ff7960c1106941caf210db104a Mon Sep 17 00:00:00 2001 From: KetineniM <53168935+KetineniM@users.noreply.github.com> Date: Fri, 28 Apr 2023 17:41:03 +0530 Subject: [PATCH 03/68] UIIN-1575 JEST/RTL test cases for InstanceFilters (#2132) * UIIN-1575 JEST/RTL test cases for InstanceFilters * Update InstanceFilters.test.js Issues resolved * Update InstanceFilters.test.js removed unwanted import statements * Update InstanceFilters.test.js Issues resolved --- .../InstanceFilters/InstanceFilters.test.js | 181 ++++++++++++++++-- 1 file changed, 161 insertions(+), 20 deletions(-) diff --git a/src/components/InstanceFilters/InstanceFilters.test.js b/src/components/InstanceFilters/InstanceFilters.test.js index c1919e66d..eb2e4d8b1 100644 --- a/src/components/InstanceFilters/InstanceFilters.test.js +++ b/src/components/InstanceFilters/InstanceFilters.test.js @@ -1,20 +1,59 @@ import React from 'react'; +import '../../../test/jest/__mock__'; import { BrowserRouter as Router } from 'react-router-dom'; -import { noop } from 'lodash'; +import { screen, waitFor } from '@testing-library/react'; +import { ModuleHierarchyProvider } from '@folio/stripes-core/src/components/ModuleHierarchy'; -import '../../../test/jest/__mock__'; +import userEvent from '@testing-library/user-event'; +import renderWithIntl from '../../../test/jest/helpers/renderWithIntl'; +import { + FACETS +} from '../../constants'; +import InstanceFilters from './InstanceFilters'; +import translationsProperties from '../../../test/jest/helpers/translationsProperties'; -import { ModuleHierarchyProvider } from '@folio/stripes/core'; +jest.mock('../CheckboxFacet/CheckboxFacet', () => jest.fn().mockReturnValue('CheckboxFacet')); -import renderWithIntl from '../../../test/jest/helpers/renderWithIntl'; +jest.mock('../../facetUtils', () => ({ + ...jest.requireActual('../../facetUtils'), + getSourceOptions: jest.fn(), + getSuppressedOptions: jest.fn(), +})); -import InstanceFilters from './InstanceFilters'; +const activeFilters = { + [FACETS.EFFECTIVE_LOCATION]: ['loc1'], + [FACETS.ITEM_STATUS]: ['ITEM_STATUS1'], + [FACETS.RESOURCE]: ['RESOURCE1'], + [FACETS.FORMAT]: ['Format1'], + [FACETS.MODE]: ['Mode1'], + [FACETS.NATURE_OF_CONTENT]: ['NATUREOFCONTENT1'], + [FACETS.STAFF_SUPPRESS]: ['STAFFSUPPRESS1'], + [FACETS.INSTANCES_DISCOVERY_SUPPRESS]: ['DISCOVERYSUPPRESS1'], + [FACETS.STATISTICAL_CODE_IDS]: ['STATISTICALCODEIDS1'], + [FACETS.CREATED_DATE]: ['2022-01-01'], + [FACETS.UPDATED_DATE]: ['2022-01-01'], + [FACETS.STATUS]: ['STATUS1'], + [FACETS.SOURCE]: ['SOURCE1'] +}; const resources = { facets: { hasLoaded: true, resource: 'facets', - records: [], + records: [{ + 'items.effectiveLocationId': 'effectiveLocationId1', + 'languages': 'languages', + 'statisticalCodeIds': 'statisticalCodeIds1', + 'discoverySuppress': 'discoverySuppress1', + 'source': 'source1', + 'instanceTags': 'instanceTags1', + 'statusId': 'statusId1', + 'staffSuppress': 'staffSuppress1', + 'natureOfContentTermIds': 'natureOfContentTermIds1', + 'modeOfIssuanceId': 'modeOfIssuanceId1', + 'instanceFormatIds': 'instanceFormatIds1', + 'instanceTypeId': 'instanceTypeId1', + }], other: { totalRecords: 0 } }, }; @@ -22,46 +61,148 @@ const resources = { const data = { locations: [], resourceTypes: [], - statisticalCodes: [], instanceFormats: [], modesOfIssuance: [], + statisticalCodes: [], tagsRecords: [], natureOfContentTerms: [], query: [], - onFetchFacets: noop, + onFetchFacets: jest.fn(), parentResources: resources, }; - +const onChange = jest.fn(); +const onClear = jest.fn(); const renderInstanceFilters = () => { return renderWithIntl( - + , + translationsProperties ); }; describe('InstanceFilters', () => { - beforeEach(() => { + it('Should Clear selected filters for effective Location', async () => { renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[1]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + it('Should Clear selected filters for language', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[3]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for resource', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[5]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); }); - it('Contains a filter for creation date ', () => { - expect(document.querySelector('#createdDate')).toBeInTheDocument(); + it('Should Clear selected filters for format', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[7]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); }); - it('Contains a filter for update date ', () => { - expect(document.querySelector('#updatedDate')).toBeInTheDocument(); + it('Should Clear selected filters for mode', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[9]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for nature Of Content', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[11]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); }); - it('Contains a filter for statistical code', () => { - expect(document.querySelector('#statisticalCodeIds')).toBeInTheDocument(); + it('Should Clear selected filters for staffSuppress', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[13]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for Suppress from discovery', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[15]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for Statistical code filter list', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[17]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for Date created filter list', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[19]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for Date updated filter list', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[26]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for Instance status filter list', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[33]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); + }); + + it('Should Clear selected filters for Source filter list', async () => { + renderInstanceFilters(); + const Clearselectedfilters = screen.getAllByRole('button'); + userEvent.click(Clearselectedfilters[35]); + await waitFor(() => { + expect(onClear).toBeCalled(); + }); }); }); From 3d1d9de039ef20d804f75ab125e20d5f52161d38 Mon Sep 17 00:00:00 2001 From: cchitneedi <100114810+cchitneedi@users.noreply.github.com> Date: Fri, 28 Apr 2023 18:24:36 +0530 Subject: [PATCH 04/68] UIIN-1782 create Jest/RTL test for SubInstanceList.js (#2114) * UIIN-1782 create Jest/RTL test for SubInstanceList.js * UIIN-1782 create Jest/RTL test for SubInstanceList.js --- .../SubInstanceList/SubInstanceList.test.js | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/Instance/InstanceDetails/SubInstanceList/SubInstanceList.test.js diff --git a/src/Instance/InstanceDetails/SubInstanceList/SubInstanceList.test.js b/src/Instance/InstanceDetails/SubInstanceList/SubInstanceList.test.js new file mode 100644 index 000000000..a47570c0a --- /dev/null +++ b/src/Instance/InstanceDetails/SubInstanceList/SubInstanceList.test.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import '../../../../test/jest/__mock__'; +import { Router } from 'react-router'; +import userEvent from '@testing-library/user-event'; +import { createMemoryHistory } from 'history'; +import { renderWithIntl, translationsProperties } from '../../../../test/jest/helpers'; +import useLoadSubInstances from '../../../hooks/useLoadSubInstances'; + +import SubInstanceList from './SubInstanceList'; + +const mockuseLoadSubInstancesVaues = [ + { + id: '1', + title: 'Test Title 1', + hrid: 'TestHrid1', + publication: [{ publisher: 'Test Publisher 1', dateOfPublication: '04-17-2023' }], + identifiers: [ + { identifierTypeId: 'ISBN', value: '369369' }, + ] + }, + { + id: '2', + title: 'Test Title 2', + hrid: 'TestHrid2', + publication: [{ publisher: 'Test Publisher 2', dateOfPublication: '04-17-2023' }], + identifiers: [ + { identifierTypeId: 'ISSN', value: '789789' }, + ], + } +]; + +const history = createMemoryHistory(); +const defaultProps = { + id: 'TestID', + label: 'LabelTest', + titles: [{}], + titleKey: 'id', +}; + +const queryClient = new QueryClient(); +const renderSubInstanceList = (props) => renderWithIntl( + + + + + , + translationsProperties +); + + +jest.mock('../../../hooks/useReferenceData', () => jest.fn().mockReturnValue({ + identifierTypesById : { + 'ISBN': { name : 'ISBN' }, + 'ISSN': { name : 'ISSN' } + } +})); +jest.mock('../../../hooks/useLoadSubInstances', () => jest.fn()); + + +describe('render SubInstanceList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('NoValue should display when no title is present', () => { + useLoadSubInstances.mockReturnValue([{}]); + const { getAllByText } = renderSubInstanceList(defaultProps); + expect(getAllByText(/No value set/i).length).toBe(4); + }); + it('No link to be present when tittleKey is empty', () => { + useLoadSubInstances.mockReturnValue(mockuseLoadSubInstancesVaues); + const { container, getByText } = renderSubInstanceList({ ...defaultProps, titleKey: '' }); + expect(getByText('Test Title 1')).toBeInTheDocument(); + expect(getByText('Test Title 1')).not.toHaveAttribute('href'); + expect(getByText('Test Title 2')).toBeInTheDocument(); + expect(getByText('Test Title 2')).not.toHaveAttribute('href'); + expect(container.getElementsByTagName('a').length).toBe(0); + }); + it('Link to a title to be present when title and tittleKey is present', () => { + useLoadSubInstances.mockReturnValue(mockuseLoadSubInstancesVaues); + const { container, getByText } = renderSubInstanceList(defaultProps); + expect(getByText('Test Title 1')).toHaveAttribute('href', '/inventory/view/1'); + expect(getByText('Test Title 2')).toHaveAttribute('href', '/inventory/view/2'); + expect(container.getElementsByTagName('a').length).toBe(2); + }); + it('Pagination message to be render on clicking next button', () => { + useLoadSubInstances.mockReturnValue(mockuseLoadSubInstancesVaues); + const { container, getByRole } = renderSubInstanceList(defaultProps); + userEvent.click(getByRole('button', { name: 'Next' })); + expect(container.getElementsByClassName('sr-only').length).toBe(1); + }); +}); From 366c69d229fade2882bcf316a084d81c694a231f Mon Sep 17 00:00:00 2001 From: cchitneedi <100114810+cchitneedi@users.noreply.github.com> Date: Fri, 28 Apr 2023 18:46:15 +0530 Subject: [PATCH 05/68] UIIN-1773 create Jest/RTL test for TitleSeriesStatements.js (#2116) --- .../TitleSeriesStatements.test.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/Instance/InstanceDetails/InstanceTitleData/TitleSeriesStatements.test.js diff --git a/src/Instance/InstanceDetails/InstanceTitleData/TitleSeriesStatements.test.js b/src/Instance/InstanceDetails/InstanceTitleData/TitleSeriesStatements.test.js new file mode 100644 index 000000000..b33b6216c --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceTitleData/TitleSeriesStatements.test.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { screen } from '@testing-library/react'; + +import '../../../../test/jest/__mock__'; +import renderWithIntl from '../../../../test/jest/helpers/renderWithIntl'; + +import TitleSeriesStatements from './TitleSeriesStatements'; + +const seriesStatements = ['Statement 1', 'Statement 2']; + +jest.mock('../ControllableDetail', () => ({ + ControllableDetail: jest.fn().mockReturnValue('ControllableDetail'), +})); + +const props = { + seriesStatements, + segment: 'segments', + source: 'source-test' +}; + +const renderTitleSeriesStatements = () => ( + renderWithIntl( + + + + ) +); + +describe('TitleSeriesStatements', () => { + it('Should renders correctly', () => { + const { getByRole } = renderTitleSeriesStatements(); + const list = getByRole('grid'); + expect(list).toBeInTheDocument(); + expect(list).toHaveAttribute('id', 'list-series-statement'); + expect(screen.getByText(/ui-inventory.seriesStatement/i)).toBeInTheDocument(); + expect(screen.getAllByText(/ControllableDetail/i)).toBeTruthy(); + }); +}); From d9585990339d943388859bf81a8416cac4f68422 Mon Sep 17 00:00:00 2001 From: FOLIO Translations Bot <38661258+folio-translations@users.noreply.github.com> Date: Fri, 28 Apr 2023 17:42:36 -0400 Subject: [PATCH 06/68] Lokalise: updates --- translations/ui-inventory/ar.json | 3 ++- translations/ui-inventory/ber.json | 3 ++- translations/ui-inventory/ca.json | 3 ++- translations/ui-inventory/cs_CZ.json | 15 ++++++++------- translations/ui-inventory/da.json | 3 ++- translations/ui-inventory/de.json | 9 +++++---- translations/ui-inventory/en_GB.json | 3 ++- translations/ui-inventory/en_SE.json | 3 ++- translations/ui-inventory/en_US.json | 3 ++- translations/ui-inventory/es.json | 3 ++- translations/ui-inventory/es_419.json | 15 ++++++++------- translations/ui-inventory/es_ES.json | 3 ++- translations/ui-inventory/fr.json | 3 ++- translations/ui-inventory/fr_FR.json | 3 ++- translations/ui-inventory/he.json | 3 ++- translations/ui-inventory/hi_IN.json | 3 ++- translations/ui-inventory/hu.json | 3 ++- translations/ui-inventory/it_IT.json | 3 ++- translations/ui-inventory/ja.json | 3 ++- translations/ui-inventory/ko.json | 3 ++- translations/ui-inventory/nb.json | 3 ++- translations/ui-inventory/nn.json | 3 ++- translations/ui-inventory/pl.json | 3 ++- translations/ui-inventory/pt_BR.json | 7 ++++--- translations/ui-inventory/pt_PT.json | 3 ++- translations/ui-inventory/ru.json | 3 ++- translations/ui-inventory/sv.json | 3 ++- translations/ui-inventory/ur.json | 3 ++- translations/ui-inventory/zh_CN.json | 9 +++++---- translations/ui-inventory/zh_TW.json | 3 ++- 30 files changed, 80 insertions(+), 50 deletions(-) diff --git a/translations/ui-inventory/ar.json b/translations/ui-inventory/ar.json index 10542560a..a7d7f641b 100644 --- a/translations/ui-inventory/ar.json +++ b/translations/ui-inventory/ar.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/ber.json b/translations/ui-inventory/ber.json index efc7049ec..6edccc4aa 100644 --- a/translations/ui-inventory/ber.json +++ b/translations/ui-inventory/ber.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/ca.json b/translations/ui-inventory/ca.json index 76d244f33..d66f93ea5 100644 --- a/translations/ui-inventory/ca.json +++ b/translations/ui-inventory/ca.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/cs_CZ.json b/translations/ui-inventory/cs_CZ.json index eb3aa10be..200330959 100644 --- a/translations/ui-inventory/cs_CZ.json +++ b/translations/ui-inventory/cs_CZ.json @@ -371,9 +371,9 @@ "formatCode": "Kód formátu", "addNatureOfContentTerm": "Přidat povahu obsahu", "selectNatureOfContentTerm": "Vyberte povahu obsahu", - "hridHandling": "Přidělování HRID", - "hridHandling.description.line1": "Po úvodní migraci dat jsou nové FOLIO HRID přidělovány sekvenčně podle stavu počitadla v těchto nastaveních.", - "hridHandling.description.line2": "Pokud se nezmění nebo neodstraní, bude novým identifikátorům FOLIO HRID přidělen výchozí prefix.", + "hridHandling": "Zpracování HRID", + "hridHandling.description.line1": "Po úvodní migraci dat jsou nové FOLIO HRID přiřazovány sekvenčně podle stavu počitadla v těchto nastaveních.", + "hridHandling.description.line2": "Pokud se nezmění nebo neodstraní, bude novým identifikátorům FOLIO HRID přiřazen výchozí prefix.", "hridHandling.description.line3": "HRID ve stávajícím katalogu FOLIO a MARC záznamech nelze změnit", "hridHandling.sectionHeader1": "Záznamy titulů a MARC bibliografické záznamy", "hridHandling.sectionHeader2": "Záznamy holdingů a MARC holdingové záznamy", @@ -512,7 +512,7 @@ "permission.settings.call-number-types": "Nastavení (Katalog): Vytvoření, úprava a smazání typů signatur", "permission.settings.holdings-note-types": "Nastavení (Katalog): Vytvářet, upravit a mazat typy poznámek holdingů", "permission.settings.item-note-types": "Nastavení (Katalog): Vytváření, úpravy, smazání typů poznámek k jednotkám", - "permission.settings.hrid-handling": "Nastavení (Katalog): Vytvoření, úprava a smazání přidělování HRID", + "permission.settings.hrid-handling": "Nastavení (Katalog): Vytvoření, úprava a smazání zpracování HRID", "permission.instance.view": "Katalog: Zobrazit tituly, holdingy a jednotky", "permission.instance.create": "Katalog: Zobrazení, vytvoření titulů", "permission.instance.edit": "Katalog: Zobrazení, vytvoření, úprava titulů", @@ -774,9 +774,10 @@ "copycat.overlayJobProfileToBeUsed": "Vyberte profil, který se má použít k překrytí aktuálních dat", "effectiveLocationHoldings": "Efektivní lokace pro holdingy", "info.effectiveCallNumber": "Toto pole obsahuje signaturu jednotky, která je buď zděděna ze záznamu holdingu, nebo je aktualizovanou signaturou záznamu jednotky. Při procházení signatur se bude vyhledávat v tomto poli.", - "info.shelvingOrder": "Toto pole je normalizovaná forma signatury, která určuje, jak je signatura při procházení tříděna.", + "info.shelvingOrder": "Toto pole je normalizovaná forma signatury, která určuje, jak je signatura při procházení seřazena.", "markAsHeader": "Označit jako", "newMARCRecord": "Nový záznam MARC Bib", - "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.add": "Přidat Vazbu a analitiku", + "boundWithTitles.enterHoldingsHrid": "Zadejte HRID holdingů", + "selectCode": "Vybrat kód" } \ No newline at end of file diff --git a/translations/ui-inventory/da.json b/translations/ui-inventory/da.json index ba5bae5f9..df82ffced 100644 --- a/translations/ui-inventory/da.json +++ b/translations/ui-inventory/da.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/de.json b/translations/ui-inventory/de.json index 49a0c5ba9..ed9f3f77c 100644 --- a/translations/ui-inventory/de.json +++ b/translations/ui-inventory/de.json @@ -365,10 +365,10 @@ "acquisition": "Erwerbung", "holdingsNoteType": "Anmerkungstyp des Bestands", "itemNoteType": "Anmerkungstyp des Exemplars", - "formatSource": "Format-Quelle", + "formatSource": "Formatquelle", "formatTerm": "Formatbegriff", - "formatCategory": "Format-Kategorie", - "formatCode": "Format-Code", + "formatCategory": "Formatkategorie", + "formatCode": "Formatcode", "addNatureOfContentTerm": "Art des Inhalts hinzufügen", "selectNatureOfContentTerm": "Art des Inhalts auswählen", "hridHandling": "HRID-Handhabung", @@ -778,5 +778,6 @@ "markAsHeader": "Markieren als", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/en_GB.json b/translations/ui-inventory/en_GB.json index 396371beb..22950aecf 100644 --- a/translations/ui-inventory/en_GB.json +++ b/translations/ui-inventory/en_GB.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/en_SE.json b/translations/ui-inventory/en_SE.json index 396371beb..22950aecf 100644 --- a/translations/ui-inventory/en_SE.json +++ b/translations/ui-inventory/en_SE.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/en_US.json b/translations/ui-inventory/en_US.json index 2fd96c030..51be90b3a 100644 --- a/translations/ui-inventory/en_US.json +++ b/translations/ui-inventory/en_US.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/es.json b/translations/ui-inventory/es.json index ca9cc36ca..c7a53a0cf 100644 --- a/translations/ui-inventory/es.json +++ b/translations/ui-inventory/es.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/es_419.json b/translations/ui-inventory/es_419.json index 106a34be5..37af441cd 100644 --- a/translations/ui-inventory/es_419.json +++ b/translations/ui-inventory/es_419.json @@ -163,8 +163,8 @@ "addPublicationFrequency": "Añadir frecuencia", "addPublicationRange": "Añadir rango", "publicationRange": "Rango de publicación", - "staffSuppress": "Suprimir en la intranet", - "discoverySuppress": "Suprimir en el discovery", + "staffSuppress": "Ocultar en el EDS", + "discoverySuppress": "Ocultar en el EDS", "previouslyHeld": "Reservado previamente", "addElectronicAccess": "Añadir acceso electrónico", "instanceStatus": "Término de estado de la instancia", @@ -535,12 +535,12 @@ "folio": "FOLIO", "marc": "MARC", "effectiveLocationShort": "Localización efectiva", - "moveItems.move.holdings.count": "Move: {count, number} {count, plural, one {holding} other {holdings}}", - "moveItems.instance.dropZone": "Drop holding", + "moveItems.move.holdings.count": "Mover: {count, number} {count, plural, one {tener} other {tener}}", + "moveItems.instance.dropZone": "Sujeción de la gota", "moveItems.modal.confirmLabel": "Continuar", "moveItems.modal.title": "Confirmar movimiento", - "moveItems.instance.items.success": "{count, number} {count, plural, one {item has} other {items have}} been successfully moved.", - "moveItems.instance.holdings.success": "{count, number} {count, plural, one {holding has} other {holdings have}} been successfully moved.", + "moveItems.instance.items.success": "{count, number} {count, plural, one {artículo ha} other {artículos han}} sido movidos con éxito.", + "moveItems.instance.holdings.success": "{count, number} {count, plural, one {retención ha} other {retenciones han}} sido trasladadas con éxito.", "moveItems.instance.items.error": "Los siguientes ejemplares no se pudieron mover {items}. Verifique los campos del registro de la instancia e intente nuevamente o notifique a su administrador de sistemas.", "moveItems.instance.items.error.server": "Hubo un problema al actualizar los siguientes ejemplares {items}. Vuelva a intentarlo o notifique a su administrador de sistemas.", "moveItems.instance.holdings.error": "Las siguientes existencias no se pudieron mover {holdings}. Verifique los campos del registro de la instancia e intente nuevamente o notifique a su administrador de sistemas.", @@ -778,5 +778,6 @@ "markAsHeader": "Marcar como", "newMARCRecord": "Nuevo MARC Bib Record", "boundWithTitles.add": "Añadir Bound-with y análisis", - "boundWithTitles.enterHoldingsHrid": "Ingrese las participaciones HRID" + "boundWithTitles.enterHoldingsHrid": "Ingrese las participaciones HRID", + "selectCode": "Seleccionar código" } \ No newline at end of file diff --git a/translations/ui-inventory/es_ES.json b/translations/ui-inventory/es_ES.json index 0ebe34b49..e213cdb20 100644 --- a/translations/ui-inventory/es_ES.json +++ b/translations/ui-inventory/es_ES.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/fr.json b/translations/ui-inventory/fr.json index 6470cf807..f764fc5e8 100644 --- a/translations/ui-inventory/fr.json +++ b/translations/ui-inventory/fr.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/fr_FR.json b/translations/ui-inventory/fr_FR.json index 2b5f381d0..01d9e9f23 100644 --- a/translations/ui-inventory/fr_FR.json +++ b/translations/ui-inventory/fr_FR.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/he.json b/translations/ui-inventory/he.json index 2bbb4bd3c..aebae3530 100644 --- a/translations/ui-inventory/he.json +++ b/translations/ui-inventory/he.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/hi_IN.json b/translations/ui-inventory/hi_IN.json index efc7049ec..6edccc4aa 100644 --- a/translations/ui-inventory/hi_IN.json +++ b/translations/ui-inventory/hi_IN.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/hu.json b/translations/ui-inventory/hu.json index cd00cf215..9815b6271 100644 --- a/translations/ui-inventory/hu.json +++ b/translations/ui-inventory/hu.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/it_IT.json b/translations/ui-inventory/it_IT.json index 80b13d5b4..f949c3b90 100644 --- a/translations/ui-inventory/it_IT.json +++ b/translations/ui-inventory/it_IT.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/ja.json b/translations/ui-inventory/ja.json index 852c17424..ed9ff1600 100644 --- a/translations/ui-inventory/ja.json +++ b/translations/ui-inventory/ja.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/ko.json b/translations/ui-inventory/ko.json index 659cdef1b..83701cebf 100644 --- a/translations/ui-inventory/ko.json +++ b/translations/ui-inventory/ko.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/nb.json b/translations/ui-inventory/nb.json index efc7049ec..6edccc4aa 100644 --- a/translations/ui-inventory/nb.json +++ b/translations/ui-inventory/nb.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/nn.json b/translations/ui-inventory/nn.json index efc7049ec..6edccc4aa 100644 --- a/translations/ui-inventory/nn.json +++ b/translations/ui-inventory/nn.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/pl.json b/translations/ui-inventory/pl.json index c3642d0b5..7e3b1af29 100644 --- a/translations/ui-inventory/pl.json +++ b/translations/ui-inventory/pl.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/pt_BR.json b/translations/ui-inventory/pt_BR.json index 981d6a905..ec14cd773 100644 --- a/translations/ui-inventory/pt_BR.json +++ b/translations/ui-inventory/pt_BR.json @@ -229,7 +229,7 @@ "addHoldingsStatementForIndexes": "Adicionar declaração de coleções para índices", "addHoldingsStatementForSupplements": "Adicionar declaração de coleções para suplementos", "relatedInstances": "Instâncias relacionadas", - "instanceRelationshipsAnalyticsBoundWith": "Relacionamentos entre instâncias (analiticas e encadernado com)", + "instanceRelationshipsAnalyticsBoundWith": "Relacionamentos entre instâncias (analíticas e encadernado com)", "instanceStatusTypes": "Tipos de status da instância", "instanceStatusType": "Tipo de status da instância", "instanceHoldingsItem": "Instâncias, Coleções, Itens", @@ -777,6 +777,7 @@ "info.shelvingOrder": "Este campo é a forma normalizada do número de chamada que determina como o número de chamada é classificado durante a navegação.", "markAsHeader": "Marcar como", "newMARCRecord": "Novo registro MARC Bibliográfico", - "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.add": "Adicione encadernados e analíticas", + "boundWithTitles.enterHoldingsHrid": "Digite o HRID das Coleções", + "selectCode": "Selecione o código" } \ No newline at end of file diff --git a/translations/ui-inventory/pt_PT.json b/translations/ui-inventory/pt_PT.json index 5ebfcad2a..2b210cc58 100644 --- a/translations/ui-inventory/pt_PT.json +++ b/translations/ui-inventory/pt_PT.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/ru.json b/translations/ui-inventory/ru.json index 2000f3c9d..a4d134341 100644 --- a/translations/ui-inventory/ru.json +++ b/translations/ui-inventory/ru.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/sv.json b/translations/ui-inventory/sv.json index efc7049ec..6edccc4aa 100644 --- a/translations/ui-inventory/sv.json +++ b/translations/ui-inventory/sv.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/ur.json b/translations/ui-inventory/ur.json index 14f61c347..210e3514b 100644 --- a/translations/ui-inventory/ur.json +++ b/translations/ui-inventory/ur.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file diff --git a/translations/ui-inventory/zh_CN.json b/translations/ui-inventory/zh_CN.json index ab7069eaf..f5d7ca7fa 100644 --- a/translations/ui-inventory/zh_CN.json +++ b/translations/ui-inventory/zh_CN.json @@ -614,8 +614,8 @@ "internalIdEmbedPath": "内部ID嵌入路径", "copycat.import": "导入", "copycat.reimport": "重新导入", - "copycat.callout.updated": "已更新记录{xid}", - "copycat.callout.created": "已创建记录{xid}", + "copycat.callout.updated": "记录{xid}已更新。结果可能需要一些时间才能在典藏中显示", + "copycat.callout.created": "记录{xid}已创建。结果可能需要一些时间才能在典藏中显示", "copycat.callout.simpleError": "发生错误: {err}", "copycat.callout.complexError": "发生错误: {err} : {detail}", "permission.single-record-import": "典藏:导入单个书目记录", @@ -777,6 +777,7 @@ "info.shelvingOrder": "本字段是索书号的规范化形式,它决定浏览时索书号的排序方式。", "markAsHeader": "标记为", "newMARCRecord": "新建MARC书目记录", - "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.add": "添加合订与分析", + "boundWithTitles.enterHoldingsHrid": "输入馆藏HRID", + "selectCode": "选择代码" } \ No newline at end of file diff --git a/translations/ui-inventory/zh_TW.json b/translations/ui-inventory/zh_TW.json index 91313a3c1..1d0f5cc3c 100644 --- a/translations/ui-inventory/zh_TW.json +++ b/translations/ui-inventory/zh_TW.json @@ -778,5 +778,6 @@ "markAsHeader": "Mark as", "newMARCRecord": "New MARC Bib Record", "boundWithTitles.add": "Add Bound-with and analytics", - "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID" + "boundWithTitles.enterHoldingsHrid": "Enter Holdings HRID", + "selectCode": "Select code" } \ No newline at end of file From e0eada87dae36644d45fa56be0625361615d9f3c Mon Sep 17 00:00:00 2001 From: KetineniM <53168935+KetineniM@users.noreply.github.com> Date: Fri, 5 May 2023 11:17:12 +0530 Subject: [PATCH 07/68] UIIN-1586 JEST/RTL test cases for itemFilterRenderer (#2130) --- .../ItemFilters/itemFilterRenderer.test.js | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/components/ItemFilters/itemFilterRenderer.test.js b/src/components/ItemFilters/itemFilterRenderer.test.js index b7e5fcce7..93a5afbc6 100644 --- a/src/components/ItemFilters/itemFilterRenderer.test.js +++ b/src/components/ItemFilters/itemFilterRenderer.test.js @@ -2,6 +2,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { noop } from 'lodash'; +import userEvent from '@testing-library/user-event'; import '../../../test/jest/__mock__'; @@ -9,6 +10,18 @@ import itemFilterRenderer from './itemFilterRenderer'; import renderWithIntl from '../../../test/jest/helpers/renderWithIntl'; import translationsProperties from '../../../test/jest/helpers/translationsProperties'; +jest.mock('./ItemFilters', () => ({ onClear }) => ( +
+ +
+)); + const DATA = { materialTypes: [], locations: [], @@ -25,27 +38,9 @@ const renderFilters = (data = DATA, onChange = noop) => (renderWithIntl( describe('itemFilterRenderer fn', () => { beforeEach(() => renderFilters()); - it('should display filter by tags accordion', () => { - expect(screen.getByText('Tags')).toBeInTheDocument(); - }); - - it('should display filter by status accordion', () => { - expect(screen.getByText('Item status')).toBeInTheDocument(); - }); - - it('should display filter by effectiveLocation accordion', () => { - expect(screen.getByText('Effective location (item)')).toBeInTheDocument(); - }); - - it('should display filter by permanentLocation accordion', () => { - expect(screen.getByText('Holdings permanent location')).toBeInTheDocument(); - }); - - it('should display filter by materialType accordion', () => { - expect(screen.getByText('Material type')).toBeInTheDocument(); - }); - - it('should display filter by discoverySuppress accordion', () => { - expect(screen.getByText('Suppress from discovery')).toBeInTheDocument(); + it('should click the clearButton', () => { + const clearButton = screen.getByTestId('onClear'); + expect(clearButton).toBeInTheDocument(); + userEvent.click(clearButton); }); }); From a7d2dbcfed098ba3d7ee85a00dfcc97665274f21 Mon Sep 17 00:00:00 2001 From: VSnehalatha <53073086+VSnehalatha@users.noreply.github.com> Date: Fri, 5 May 2023 15:31:13 +0530 Subject: [PATCH 08/68] UIIN-1778 Create Jest/RTL test for ViewRequests.js (#2097) * Create Jest/RTL test for ViewRequests.js * Update ViewRequests.test.js --- .../ViewRequests/ViewRequests.test.js | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/Instance/ViewRequests/ViewRequests.test.js diff --git a/src/Instance/ViewRequests/ViewRequests.test.js b/src/Instance/ViewRequests/ViewRequests.test.js new file mode 100644 index 000000000..259011196 --- /dev/null +++ b/src/Instance/ViewRequests/ViewRequests.test.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import '../../../test/jest/__mock__'; +import renderWithIntl from '../../../test/jest/helpers/renderWithIntl'; + +import ViewRequests from './ViewRequests'; + +const items = [ + { + barcode: '1234', + status: { name: 'available' }, + id: '1', + holdingsRecordId: '2', + materialType: { name: 'book' }, + temporaryLoanType: { name: 'temporary' }, + effectiveLocation: { name: 'Main Library' }, + enumeration: 'vol 1', + chronology: '2001' + }, +]; +const requestsMap = new Map([['1', '1']]); +const loansMap = new Map([['1', [{ id: '1', dueDate: '2023-03-31T20:00:00.000Z' }]]]); +const instanceId = '1'; +const instance = { title: 'Book Title', publication: [{ dateOfPublication: '2022' }] }; + +const onCloseViewRequests = jest.fn(); + +const ViewRequestsSetup = () => ( + + + +); + +describe('ViewRequests', () => { + beforeEach(() => { + renderWithIntl(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render paneTitles', () => { + expect(screen.getByText(/ui-inventory.instanceRecordRequestsTitle/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.instanceRecordRequestsSubtitle/i)).toBeInTheDocument(); + }); + it('should render the correct column headers', () => { + expect(screen.getByText(/ui-inventory.item.barcode/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.status/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.item.availability.dueDate/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.item.requestQueue/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.effectiveLocationShort/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.loanType/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.enumeration/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.chronology/i)).toBeInTheDocument(); + expect(screen.getByText(/ui-inventory.materialType/i)).toBeInTheDocument(); + }); + it('should render the correct data for each item', () => { + expect(screen.getByText(/1234/i)).toBeInTheDocument(); + expect(screen.getByText(/available/i)).toBeInTheDocument(); + expect(screen.getByText('3/31/2023, 8:00 PM')).toBeInTheDocument(); + expect(screen.getByText('1')).toBeInTheDocument(); + expect(screen.getByText(/Main Library/i)).toBeInTheDocument(); + expect(screen.getByText(/temporary/i)).toBeInTheDocument(); + expect(screen.getByText(/vol 1/i)).toBeInTheDocument(); + expect(screen.getByText(/2001/i)).toBeInTheDocument(); + expect(screen.getByText(/book/i)).toBeInTheDocument(); + }); + it('should click barcodeButton', () => { + const barcodeButton = screen.getByRole('button', { name: 'ui-inventory.item.barcode' }); + expect(barcodeButton).toBeInTheDocument(); + userEvent.click(barcodeButton); + }); + it('should click requestQueue', () => { + const requestQueueButton = screen.getByRole('button', { name: 'ui-inventory.item.requestQueue' }); + expect(requestQueueButton).toBeInTheDocument(); + userEvent.click(requestQueueButton); + }); +}); From ea38daca04dbf825e442da5a58270e0322485adb Mon Sep 17 00:00:00 2001 From: Mariia Aloshyna <55138456+mariia-aloshyna@users.noreply.github.com> Date: Fri, 5 May 2023 15:19:04 +0300 Subject: [PATCH 09/68] UIIN-2338: The 'Missing' and 'Withdrawn' options in the actions menu are absent after clicking on the 'Actions' button for item status "In process" (#2142) --- CHANGELOG.md | 2 +- src/views/ItemView.js | 258 ++++++++++++++++++++++-------------------- 2 files changed, 137 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784fa38f5..2fac72619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ * Holdings Create/Edit screens: Replace custom RepeatableField with component from stripes. Refs UIIN-2398. * Holdings Create/Edit screens: Repeatable field trashcan is not aligned with the data row. Fixes UIIN-2373. * Holdings view source: Print button not visible with "View MARC holdings record" permission. Refs UIIN-2405. - +* The 'Missing' and 'Withdrawn' options in the actions menu are absent after clicking on the 'Actions' button for item status "In process". Fixes UIIN-2338. ## [9.4.5](https://github.com/folio-org/ui-inventory/tree/v9.4.5) (2023-04-03) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.4...v9.4.5) diff --git a/src/views/ItemView.js b/src/views/ItemView.js index 9b07a8bf0..520d953bd 100644 --- a/src/views/ItemView.js +++ b/src/views/ItemView.js @@ -252,146 +252,160 @@ class ItemView extends React.Component { const firstItem = get(resources, 'itemsResource.records[0]'); const request = get(resources, 'requests.records[0]'); const newRequestLink = `/requests?itemId=${firstItem.id}&query=${firstItem.id}&layer=create`; - const canCreate = stripes.hasPerm('ui-inventory.item.create'); - const canEdit = stripes.hasPerm('ui-inventory.item.edit'); - const canMarkAsMissing = stripes.hasPerm('ui-inventory.item.markasmissing'); - const canDelete = stripes.hasPerm('ui-inventory.item.delete'); - const canDisplayActionMenu = actionMenuDisplayPerms.some(perm => stripes.hasPerm(perm)); - - if (!canDisplayActionMenu) { + const userHasPermToCreate = stripes.hasPerm('ui-inventory.item.create'); + const userHasPermToEdit = stripes.hasPerm('ui-inventory.item.edit'); + const userHasPermToMarkAsMissing = stripes.hasPerm('ui-inventory.item.markasmissing'); + const userHasPermToMarkAsWithdrawn = stripes.hasPerm('ui-inventory.items.mark-items-withdrawn'); + const userHasPermToDelete = stripes.hasPerm('ui-inventory.item.delete'); + const userHasPermToObserveActionMenu = actionMenuDisplayPerms.some(perm => stripes.hasPerm(perm)); + + const userCanMarkItemAsMissing = canMarkItemAsMissing(firstItem); + const userCanMarkItemAsWithdrawn = canMarkItemAsWithdrawn(firstItem); + const userCanMarkItemWithStatus = canMarkItemWithStatus(firstItem); + const userCanCreateNewRequest = canCreateNewRequest(firstItem, stripes); + + if (!userHasPermToObserveActionMenu) { return null; } - return ( - <> - - { canEdit && ( - - )} - { canCreate && ( + const editActionItem = ( + + ); + const duplicateActionItem = ( + + ); + const deleteActionItem = ( + + ); + const newRequestActionItem = ( + + ); + const markAsMissingActionItem = ( + + ); + const markAsWithdrawnActionItem = ( + + ); + const renderItemStatusActionItems = () => Object.keys(itemStatusMutators) + .filter(status => itemStatusesMap[status] !== firstItem?.status?.name) + .map(status => { + const itemStatus = itemStatusesMap[status]; + const parameterizedStatus = parameterize(itemStatus); + + const actionMenuItem = ( - )} - { canDelete && ( - - )} - { canCreateNewRequest(firstItem, stripes) && ( - - )} + {actionMenuItem} + + ); + }); + + const isMarkAsMenuSectionVisible = (userCanMarkItemAsMissing && userHasPermToMarkAsMissing) + || (userHasPermToMarkAsWithdrawn && userCanMarkItemAsWithdrawn) + || userCanMarkItemWithStatus; + + return ( + <> + + {userHasPermToEdit && editActionItem} + {userHasPermToCreate && duplicateActionItem} + {userHasPermToDelete && deleteActionItem} + {userCanCreateNewRequest && newRequestActionItem} - { canMarkItemWithStatus(firstItem) && ( + {isMarkAsMenuSectionVisible && ( } labelTag="h3" > - { canMarkItemAsMissing(firstItem) && canMarkAsMissing && ( - - )} - - { canMarkItemAsWithdrawn(firstItem) && ( - - )} - - { - Object.keys(itemStatusMutators) - .filter(status => itemStatusesMap[status] !== firstItem?.status?.name) - .map(status => { - const itemStatus = itemStatusesMap[status]; - const parameterizedStatus = parameterize(itemStatus); - - const actionMenuItem = ( - - ); - return ( - - {actionMenuItem} - - ); - }) - } + {userCanMarkItemAsMissing && userHasPermToMarkAsMissing && markAsMissingActionItem} + {userCanMarkItemAsWithdrawn && userHasPermToMarkAsWithdrawn && markAsWithdrawnActionItem} + {userCanMarkItemWithStatus && renderItemStatusActionItems()} )} From 48aa3f76c5d3f8227d76a39c6f0ff06b569a43ed Mon Sep 17 00:00:00 2001 From: Mariia Aloshyna <55138456+mariia-aloshyna@users.noreply.github.com> Date: Fri, 5 May 2023 22:53:12 +0300 Subject: [PATCH 10/68] UIIN-2397: Item Create/Edit screens: Replace custom RepeatableField with component from stripes (#2137) --- CHANGELOG.md | 2 + package.json | 2 +- src/components/RepeatableField/FieldRow.js | 270 ------------------ .../RepeatableField/RepeatableField.css | 7 - .../RepeatableField/RepeatableField.js | 127 -------- src/components/RepeatableField/index.js | 1 - src/components/RepeatableField/readme.md | 85 ------ src/components/index.js | 1 - src/edit/InstanceForm.js | 19 +- src/edit/items/BoundWithFieldRow.js | 57 ---- src/edit/items/BoundWithModal.js | 1 + src/edit/items/ItemForm.js | 202 ++----------- src/edit/items/ItemForm.test.js | 108 +------ .../repeatableFields/BoundWithTitlesFields.js | 165 +++++++++++ .../CirculationNotesFields.js | 124 ++++++++ .../FormerIdentifierFields.js | 66 +++++ .../repeatableFields/YearCaptionFields.js | 66 +++++ src/edit/items/repeatableFields/index.js | 4 + .../tests/BoundWithTitlesFields.test.js | 179 ++++++++++++ src/edit/noteFields.js | 39 ++- src/edit/noteFields.test.js | 28 +- src/hooks/index.js | 1 + src/hooks/useBoundWithTitlesByHrids/index.js | 1 + .../useBoundWithTitlesByHrids.js | 48 ++++ .../useBoundWithTitlesByHrids.test.js | 56 ++++ test/jest/helpers/index.js | 1 + test/jest/helpers/renderWithFinalForm.js | 37 +++ 27 files changed, 852 insertions(+), 845 deletions(-) delete mode 100644 src/components/RepeatableField/FieldRow.js delete mode 100644 src/components/RepeatableField/RepeatableField.css delete mode 100644 src/components/RepeatableField/RepeatableField.js delete mode 100644 src/components/RepeatableField/index.js delete mode 100644 src/components/RepeatableField/readme.md delete mode 100644 src/edit/items/BoundWithFieldRow.js create mode 100644 src/edit/items/repeatableFields/BoundWithTitlesFields.js create mode 100644 src/edit/items/repeatableFields/CirculationNotesFields.js create mode 100644 src/edit/items/repeatableFields/FormerIdentifierFields.js create mode 100644 src/edit/items/repeatableFields/YearCaptionFields.js create mode 100644 src/edit/items/repeatableFields/index.js create mode 100644 src/edit/items/repeatableFields/tests/BoundWithTitlesFields.test.js create mode 100644 src/hooks/useBoundWithTitlesByHrids/index.js create mode 100644 src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.js create mode 100644 src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.test.js create mode 100644 test/jest/helpers/renderWithFinalForm.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fac72619..bbdc63cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ * Holdings Create/Edit screens: Repeatable field trashcan is not aligned with the data row. Fixes UIIN-2373. * Holdings view source: Print button not visible with "View MARC holdings record" permission. Refs UIIN-2405. * The 'Missing' and 'Withdrawn' options in the actions menu are absent after clicking on the 'Actions' button for item status "In process". Fixes UIIN-2338. +* Item Create/Edit screens: Replace custom RepeatableField with component from stripes. Refs UIIN-2397. +* Item Create/Edit screens: Repeatable field trashcan is not aligned with the data row. Fixes UIIN-2374. ## [9.4.5](https://github.com/folio-org/ui-inventory/tree/v9.4.5) (2023-04-03) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.4...v9.4.5) diff --git a/package.json b/package.json index 1d5c31e6d..6b5f32e53 100644 --- a/package.json +++ b/package.json @@ -836,7 +836,7 @@ "lint": "eslint .", "test": "yarn run test:unit", "test:jest": "jest", - "test:unit": "jest --silent --ci --coverage", + "test:unit": "jest --ci --coverage", "test:unit:watch": "jest --watch --coverage", "test:e2e": "stripes test karma", "test:e2e:ci": "stripes test karma --karma.singleRun --karma.browsers ChromeDocker --karma.reporters mocha junit --coverage", diff --git a/src/components/RepeatableField/FieldRow.js b/src/components/RepeatableField/FieldRow.js deleted file mode 100644 index f0c587913..000000000 --- a/src/components/RepeatableField/FieldRow.js +++ /dev/null @@ -1,270 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import uniqueId from 'lodash/uniqueId'; -import { Field } from 'react-final-form'; -import { FormattedMessage } from 'react-intl'; - -import { - Button, - Icon, - Row, - Col, - omitProps, - SRStatus, -} from '@folio/stripes/components'; - -import css from './RepeatableField.css'; - -class FieldRow extends React.Component { - constructor(props) { - super(props); - - this.refIfLastRow = this.refIfLastRow.bind(this); - this.renderControl = this.renderControl.bind(this); - this.addButton = null; - this.srstatus = null; - this.action = null; - - this.addButtonId = this.props.addButtonId || uniqueId(`${this.props.label}AddButton`); - } - - componentDidMount() { - if (this.props.fields.length === 0 && this.props.addDefaultItem) { - setTimeout(() => { this.props.addDefault(this.props.fields); }, 5); - } - } - - componentDidUpdate() { - const { - fields, - label, - hideAdd, - } = this.props; - - if (this.action) { - if (this.action.type === 'add') { - this.srstatus.sendMessage( - `added new ${label} field. ${fields.length} ${label} total` - ); - this.action = null; - } - if (this.action.type === 'remove') { - const { item } = this.action; - let contextualSpeech; - if (typeof item === 'string') { - contextualSpeech = this.action.item; - } else if (typeof item === 'object') { - const valueArray = []; - for (const key in item) { - if (typeof item[key] === 'string' && item[key].length < 25) { - valueArray.push(item[key]); - } - } - if (valueArray.length > 0) { - contextualSpeech = valueArray.join(' '); - } else { - contextualSpeech = this.action.index; - } - } - this.srstatus.sendMessage( - `${label} ${contextualSpeech} has been removed. ${fields.length} ${label} total` - ); - this.action = null; - if (!hideAdd) { - document.getElementById(this.addButtonId).focus(); - } - } - } - } - - handleRemove(index, item) { - this.action = { type: 'remove', item, index }; - this.props.fields.remove(index); - } - - handleAdd() { - this.action = { type: 'add' }; - } - - refIfLastRow(ref, index) { - const { fields } = this.props; - if (index === fields.length - 1) { - this.lastRow = ref; - this.props.lastRowRef(ref); - } - } - - renderControl(fields, field, fieldIndex, template, templateIndex) { - if (template.render) { - return template.render({ fields, field, fieldIndex, templateIndex }); - } - - const { name, label, ...rest } = omitProps(template, ['component', 'render', 'columnSize']); - const labelProps = {}; - if (fieldIndex === 0) { - labelProps.label = label; - } else { - labelProps['aria-label'] = `${label} ${fieldIndex}`; - } - return ( - - ); - } - - render() { - const { - addDefaultItem, - addLabel, - canAdd, - canDelete, - hideAdd, - containerRef, - fields, - label, - onAddField, - template, - } = this.props; - - const legend = ( - - {label} - - ); - - const handleButtonClick = () => { onAddField(fields); }; - - if (fields.length === 0 && !addDefaultItem && !hideAdd) { - return ( -
- { this.srstatus = ref; }} /> -
- {legend} - -
-
- ); - } - return ( -
- { this.srstatus = ref; }} /> -
- {legend} - - {fields.map((f, fieldIndex) => ( -
{ this.refIfLastRow(ref, fieldIndex); }} - > - - - - {template.map((t, i) => { - const { columnSize } = t; - const colSizes = typeof columnSize === 'object' ? columnSize : { xs: true }; - return ( - - {this.renderControl(fields, f, fieldIndex, t, i)} - - ); - })} - - - - - {([ariaLabel]) => ( - - )} - - - - {fieldIndex === fields.length - 1 && - !hideAdd && - - } -
- ))} -
-
- ); - } -} - -FieldRow.propTypes = { - addButtonId: PropTypes.string, - addDefault: PropTypes.func, - addDefaultItem: PropTypes.bool, - addLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - canAdd: PropTypes.bool, - canEdit: PropTypes.bool, - canDelete: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - hideAdd: PropTypes.bool, - containerRef: PropTypes.func, - fields: PropTypes.object, - formatter: PropTypes.func, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.node]), - lastRowRef: PropTypes.func, - newItemTemplate: PropTypes.object, - onAddField: PropTypes.func, - template: PropTypes.arrayOf(PropTypes.object), -}; - -FieldRow.defaultProps = { - canAdd: true, - canEdit: true, - canDelete: true, - hideAdd: false, -}; - -export default FieldRow; diff --git a/src/components/RepeatableField/RepeatableField.css b/src/components/RepeatableField/RepeatableField.css deleted file mode 100644 index 812eb8170..000000000 --- a/src/components/RepeatableField/RepeatableField.css +++ /dev/null @@ -1,7 +0,0 @@ -.RFFieldset { - padding: 1em 0; -} - -.RFLegend { - margin-bottom: 6px; -} diff --git a/src/components/RepeatableField/RepeatableField.js b/src/components/RepeatableField/RepeatableField.js deleted file mode 100644 index 7f0733233..000000000 --- a/src/components/RepeatableField/RepeatableField.js +++ /dev/null @@ -1,127 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cloneDeep from 'lodash/cloneDeep'; -import { FieldArray } from 'react-final-form-arrays'; -import FieldRow from './FieldRow'; - -class RepeatableField extends React.Component { - constructor(props) { - super(props); - - this.lastField = null; - - this._added = false; - this._arrayId = `${this.props.label}-fields`; - this.buildComponentFromTemplate = this.buildComponentFromTemplate.bind( - this - ); - this.addDefaultField = this.addDefaultField.bind(this); - this.handleAddField = this.handleAddField.bind(this); - } - - componentDidUpdate() { - if (this._added && this.lastRow) { - const firstInput = this.lastRow.querySelector('input, select'); - if (firstInput) { - firstInput.focus(); - this._added = false; - } - } - } - - buildComponentFromTemplate = ({ templateIndex, input, meta, ...rest }) => { - const Component = this.props.template[templateIndex].component; - return ; - }; - - addDefaultField(fields) { - if (this.props.newItemTemplate) { - fields.push(cloneDeep(this.props.newItemTemplate)); - } else { - fields.push(); - } - } - - handleAddField(fields) { - if (this.props.newItemTemplate) { - fields.push(cloneDeep(this.props.newItemTemplate)); - } else { - fields.push(); - } - this._added = true; - } - - render() { - const { - name, - template, - label, - newItemTemplate, - addDefaultItem, - addLabel, - addButtonId, - canAdd, - canEdit, - canDelete, - hideAdd, - component, - } = this.props; - - const mappedTemplate = canEdit - ? template - : template.map(field => ({ - ...field, - disabled: true, - })); - - return ( - { - this.container = ref; - }} - label={label} - newItemTemplate={newItemTemplate} - addDefault={this.addDefaultField} - addDefaultItem={addDefaultItem} - addLabel={addLabel} - addButtonId={addButtonId} - canAdd={canAdd} - canDelete={canDelete} - hideAdd={hideAdd} - lastRowRef={ref => { - this.lastRow = ref; - }} - /> - ); - } -} - -RepeatableField.propTypes = { - addButtonId: PropTypes.string, - addDefaultItem: PropTypes.bool, - addLabel: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), - name: PropTypes.string.isRequired, - newItemTemplate: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - template: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object)]), - canAdd: PropTypes.bool, - canEdit: PropTypes.bool, - canDelete: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]), - hideAdd: PropTypes.bool, - component: PropTypes.func, -}; - -RepeatableField.defaultProps = { - canAdd: true, - canEdit: true, - canDelete: true, - hideAdd: false, - component: FieldRow, -}; - -export default RepeatableField; diff --git a/src/components/RepeatableField/index.js b/src/components/RepeatableField/index.js deleted file mode 100644 index f7362b473..000000000 --- a/src/components/RepeatableField/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './RepeatableField'; diff --git a/src/components/RepeatableField/readme.md b/src/components/RepeatableField/readme.md deleted file mode 100644 index 4192e4771..000000000 --- a/src/components/RepeatableField/readme.md +++ /dev/null @@ -1,85 +0,0 @@ -# Repeatable Field -Form component for rendering arrays of editable data. - -## Usage -This example renders a list of items where multiple fields (a `` and a `` inputs being manipulated based on `field` settings, you can pass a rendering function through to a `render` key on the template object instead of using the `component` key. The function will be passed the `fields` object, `field`, `fieldIndex`, `template`, `templateIndex` for use within your render function. - -``` -const renderLanguageField = ({fields, field, fieldIndex, template, templateIndex}) => { - const languageOptions = languages.selectOptions(field); - return ( - - ); -} - -class LanguageFields extends React.Component { - render() { - return ( - - ); - } -} -``` diff --git a/src/components/index.js b/src/components/index.js index 44cca2c67..3e2ac0035 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,5 +1,4 @@ export { default as FilterNavigation } from './FilterNavigation'; -export { default as RepeatableField } from './RepeatableField'; export { default as InstanceFilters, InstanceFiltersBrowse, instanceFilterRenderer } from './InstanceFilters'; export { default as HoldingsRecordFilters, holdingsRecordFilterRenderer } from './HoldingsRecordFilters'; export { default as ItemFilters, itemFilterRenderer } from './ItemFilters'; diff --git a/src/edit/InstanceForm.js b/src/edit/InstanceForm.js index 3fcd4aa8d..718ea3d18 100644 --- a/src/edit/InstanceForm.js +++ b/src/edit/InstanceForm.js @@ -86,7 +86,7 @@ function validate(values) { } // Language not required, but must be not null if supplied - if (values.languages && values.languages.length) { + if (!isEmpty(values.languages)) { const errorList = []; values.languages.forEach((item, i) => { if (!item) { @@ -97,7 +97,7 @@ function validate(values) { } - if (values.alternativeTitles && values.alternativeTitles.length) { + if (!isEmpty(values.alternativeTitles)) { const errorList = []; values.alternativeTitles.forEach((item, i) => { const error = {}; @@ -235,7 +235,7 @@ class InstanceForm extends React.Component { const { records } = instanceBlockedFields; - if (!records || !records.length) return false; + if (isEmpty(records)) return false; const { blockedFields } = records[0]; @@ -268,6 +268,14 @@ class InstanceForm extends React.Component { }), ) : []; + const instanceNoteTypeOptions = referenceTables.instanceNoteTypes ? referenceTables.instanceNoteTypes.map( + it => ({ + label: it.name, + value: it.id, + selected: it.id === initialValues.instanceTypeId, + }), + ) : []; + const instanceStatusOptions = referenceTables.instanceStatuses ? referenceTables.instanceStatuses.map( it => ({ label: `${it.name} (${it.source}: ${it.code})`, @@ -370,7 +378,7 @@ class InstanceForm extends React.Component { } id="instanceSection01" > - {(initialValues.metadata && initialValues.metadata.createdDate) && + {(initialValues.metadata?.createdDate) && } @@ -664,7 +672,8 @@ class InstanceForm extends React.Component { canAdd={!this.isFieldBlocked('notes')} canEdit={!this.isFieldBlocked('notes')} canDelete={!this.isFieldBlocked('notes')} - instanceNoteTypes={referenceTables.instanceNoteTypes} + noteTypeOptions={instanceNoteTypeOptions} + noteTypeIdField="instanceNoteTypeId" /> { - const data = fields.value ? fields.value : []; - - // Load the holdings records matching the input HRIDs - const holdingsRecordHrids = data.map(boundWithTitle => boundWithTitle.briefHoldingsRecord.hrid); - const { isLoading: isHoldingsLoading, holdingsRecords } = useHoldingsQueryByHrids(holdingsRecordHrids); - - // Load the instance records matching the holdings' instanceIds. - const instanceIds = holdingsRecords.map(record => record.instanceId); - const instances = useInstancesQuery(instanceIds); - if (isHoldingsLoading || !instances.isSuccess) return ; - - // Enrich the data displayed in the FieldRow - const holdingsRecordsByHrid = keyBy(holdingsRecords, 'hrid'); - const instancesById = keyBy(instances.data?.instances, 'id'); - data.forEach(boundWithTitle => { - if (!boundWithTitle.briefHoldingsRecord?.id) { - const holdingsRecord = holdingsRecordsByHrid[boundWithTitle.briefHoldingsRecord?.hrid]; - - if (holdingsRecord) { - boundWithTitle.briefHoldingsRecord.id = holdingsRecord.id; - - const instance = instancesById[holdingsRecord.instanceId]; - boundWithTitle.briefInstance = { - id: instance?.id, - hrid: instance?.hrid, - title: instance?.title, - }; - } - } - }); - - return ( - - ); -}; - -BoundWithFieldRow.propTypes = { - fields: PropTypes.object, -}; - -export default BoundWithFieldRow; diff --git a/src/edit/items/BoundWithModal.js b/src/edit/items/BoundWithModal.js index 93a5e92ad..570fba12d 100644 --- a/src/edit/items/BoundWithModal.js +++ b/src/edit/items/BoundWithModal.js @@ -107,6 +107,7 @@ const BoundWithModal = ({ value={hrids[i]} onChange={handleChange} data-index={i} + autoFocus={i === 0} /> diff --git a/src/edit/items/ItemForm.js b/src/edit/items/ItemForm.js index f436afd4a..fc1baf9b5 100644 --- a/src/edit/items/ItemForm.js +++ b/src/edit/items/ItemForm.js @@ -1,5 +1,9 @@ import React, { createRef } from 'react'; -import { get, cloneDeep } from 'lodash'; +import { + get, + cloneDeep, + uniqBy, +} from 'lodash'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { @@ -39,9 +43,6 @@ import { } from '@folio/stripes/smart-components'; import { effectiveCallNumber } from '@folio/stripes/util'; -import RepeatableField from '../../components/RepeatableField'; -import BoundWithFieldRow from './BoundWithFieldRow'; - import OptimisticLockingBanner from '../../components/OptimisticLockingBanner'; import ElectronicAccessFields from '../electronicAccessFields'; import { memoize, mutators } from '../formUtils'; @@ -50,18 +51,24 @@ import { LocationSelectionWithCheck } from '../common'; import AdministrativeNoteFields from '../administrativeNoteFields'; import styles from './ItemForm.css'; import { RemoteStorageWarning } from './RemoteStorageWarning'; -import BoundWithModal from './BoundWithModal'; - +import { + BoundWithTitlesFields, + CirculationNotesFields, + FormerIdentifierFields, + YearCaptionFields +} from './repeatableFields'; +import StatisticalCodeFields from '../statisticalCodeFields'; +import NoteFields from '../noteFields'; function validate(values) { const errors = {}; const selectToContinueMsg = ; - if (!(values.materialType && values.materialType.id)) { + if (!values.materialType?.id) { errors.materialType = { id: selectToContinueMsg }; } - if (!(values.permanentLoanType && values.permanentLoanType.id)) { + if (!values.permanentLoanType?.id) { errors.permanentLoanType = { id: selectToContinueMsg }; } @@ -118,10 +125,6 @@ class ItemForm extends React.Component { this.cViewMetaData = props.stripes.connect(ViewMetaData); this.accordionStatusRef = createRef(); - - this.state = { - boundWithModalOpen: false, - }; } handleAccordionToggle = ({ id }) => { @@ -144,25 +147,10 @@ class ItemForm extends React.Component { this.props.form.change('itemDamagedStatusDate', new Date()); } - openBoundWithModal = (e) => { - e.preventDefault(); - e.stopPropagation(); - - this.setState({ boundWithModalOpen: true }); - }; - - closeBoundWithModal = () => { - this.setState({ boundWithModalOpen: false }); - } - - addBoundWiths = (newBoundWithHoldingsHrids) => { - let boundWithTitles = cloneDeep(this.props.form.getFieldState('boundWithTitles').value); - const newBoundWithTitles = newBoundWithHoldingsHrids.map(holdingsRecordHrid => ({ - briefHoldingsRecord: { hrid: holdingsRecordHrid }, - })); - boundWithTitles = boundWithTitles.concat(newBoundWithTitles); + addBoundWiths = (newBoundWithTitles) => { + let boundWithTitles = cloneDeep(this.props.form.getFieldState('boundWithTitles')?.value) || []; + boundWithTitles = uniqBy([...boundWithTitles, ...newBoundWithTitles], 'briefHoldingsRecord.hrid'); this.props.form.change('boundWithTitles', boundWithTitles); - this.closeBoundWithModal(); } getFooter = () => { @@ -475,31 +463,12 @@ class ItemForm extends React.Component { - } - template={[{ - component: TextField, - label: , - }]} - /> + - } - template={[ - { - label: , - component: Select, - dataOptions: [{ label: 'Select code', value: '' }, ...statisticalCodeOptions], - } - ]} - /> + @@ -652,15 +621,7 @@ class ItemForm extends React.Component { - } - template={[{ - component: TextField, - label: - }]} - /> + @@ -733,38 +694,11 @@ class ItemForm extends React.Component { > - } - template={[ - { - name: 'itemNoteTypeId', - label: , - component: Select, - dataOptions: [{ label: 'Select type', value: '' }, ...itemNoteTypeOptions], - required: true - }, - { - name: 'note', - label: , - component: TextArea, - rows: 1, - required: true - }, - { - name: 'staffOnly', - label: , - component: Checkbox, - type: 'checkbox', - inline: true, - vertical: true, - columnSize: { - xs: 3, - lg: 2, - } - } - ]} + @@ -832,43 +766,7 @@ class ItemForm extends React.Component { - } - template={[ - { - name: 'noteType', - label: , - component: Select, - dataOptions: [ - { label: 'Select type', value: '' }, - { label: 'Check in note', value: 'Check in' }, - { label: 'Check out note', value: 'Check out' } - ], - required: true - }, - { - name: 'note', - label: , - component: TextArea, - rows: 1, - required: true - }, - { - name: 'staffOnly', - label: , - component: Checkbox, - type: 'checkbox', - inline: true, - vertical: true, - columnSize: { - xs: 3, - lg: 2, - } - } - ]} - /> + @@ -922,51 +820,9 @@ class ItemForm extends React.Component { id="acc10" label={} > - } - canAdd={false} - canDelete={(fields, fieldIndex) => { - return fields?.value[fieldIndex]?.briefHoldingsRecord?.id !== - item?.holdingsRecordId; - }} - hideAdd - component={BoundWithFieldRow} - template={[ - { - name: 'briefInstance.hrid', - label: , - component: TextField, - disabled: true, - value: boundWithTitle => boundWithTitle.briefInstance.hrid, - }, - { - name: 'briefInstance.title', - label: , - component: TextField, - disabled: true, - }, - { - name: 'briefHoldingsRecord.hrid', - label: , - component: TextField, - disabled: true, - }, - ]} - /> - - this.addBoundWiths(newBoundWiths)} /> diff --git a/src/edit/items/ItemForm.test.js b/src/edit/items/ItemForm.test.js index ccd608d7d..6065cee66 100644 --- a/src/edit/items/ItemForm.test.js +++ b/src/edit/items/ItemForm.test.js @@ -4,14 +4,11 @@ import { QueryClient, QueryClientProvider, } from 'react-query'; -import { screen, waitFor } from '@testing-library/react'; -import user from '@testing-library/user-event'; import '../../../test/jest/__mock__'; import { StripesContext } from '@folio/stripes/core'; - import { renderWithIntl, translationsProperties, @@ -21,30 +18,9 @@ import { DataContext } from '../../contexts'; import ItemForm from './ItemForm'; -import useHoldingsQueryByHrids from '../../hooks/useHoldingsQueryByHrids'; -import useInstancesQuery from '../../hooks/useInstancesQuery'; - jest.mock('../common', () => ({ LocationSelectionWithCheck: () =>
LocationSelection
, })); -jest.mock('../../hooks/useHoldingsQueryByHrids'); -useHoldingsQueryByHrids.mockImplementation(() => { - return { - isLoading: false, - holdingsRecords: [ - { hrid: 'bw789', id: 'bw789', instanceId: 'bw789' }, - ], - }; -}); -jest.mock('../../hooks/useInstancesQuery'); -useInstancesQuery.mockImplementation(() => { - return { - isSuccess: true, - data: { - instances: [], - }, - }; -}); const mockInitialValues = { permanentLocationId: 'permanentLocationId', @@ -118,77 +94,17 @@ describe('ItemForm', () => { expect(container.querySelectorAll('form').length).toEqual(1); }); - describe('when displaying boundWithTitles rows', () => { - it('should disable delete on a directly linked holding', async () => { - const { container } = renderItemForm(); - - await waitFor(() => { - const directlyLinkedTitle = container.querySelector("*[data-test-repeater-field-row] input[value='bw123']"); - const directlyLinkedDelete = directlyLinkedTitle.closest('div[data-test-repeater-field-row]') - .getElementsByTagName('button').item(0); - expect(directlyLinkedDelete.disabled).toBeTruthy(); - }); - }); - - it('should enable delete on an indirectly linked holding', async () => { - const { container } = renderItemForm(); - - await waitFor(() => { - const indirectlyLinkedTitle = container.querySelector("*[data-test-repeater-field-row] input[value='bw456']"); - const indirectlyLinkedDelete = indirectlyLinkedTitle.closest('div[data-test-repeater-field-row]') - .getElementsByTagName('button').item(0); - expect(indirectlyLinkedDelete.disabled).toBeFalsy(); - }); - }); - }); - - describe('Bound With modal', () => { - it('should start out closed', async () => { - renderItemForm(); - - const saveButton = screen.queryByTestId('bound-with-modal-save-button'); - expect(saveButton).toBeNull(); - }); - - it('should open and close', async () => { - renderItemForm(); - - const openModalButton = await screen.findByTestId('bound-with-add-button'); - user.click(openModalButton); - - // Open the modal, test that the save button is visible - let saveButton = screen.queryByTestId('bound-with-modal-save-button'); - expect(saveButton).not.toBeNull(); - expect(saveButton).toBeVisible(); - - // Close the modal, look for the button again, test that it has disappeared - const cancelModalButton = screen.queryByTestId('bound-with-modal-cancel-button'); - user.click(cancelModalButton); - await waitFor(() => { - saveButton = screen.queryByTestId('bound-with-modal-save-button'); - expect(saveButton).toBeNull(); - }); - }); - - it('should trigger addBoundWiths when saved', async () => { - const { container } = renderItemForm(); - - // Initially there are two bound-with holdings - let rows = container.querySelectorAll('#acc10 *[data-test-repeater-field-row]'); - expect(rows.length).toEqual(2); - - const openModalButton = await screen.findByTestId('bound-with-add-button'); - user.click(openModalButton); - - const firstInput = screen.queryAllByTestId('bound-with-modal-input')[0]; - user.type(firstInput, 'bw789'); - - const saveModalButton = screen.queryByTestId('bound-with-modal-save-button'); - user.click(saveModalButton); - - // There should now be three - rows = container.querySelectorAll('#acc10 *[data-test-repeater-field-row]'); - expect(rows.length).toEqual(3); - }); + it('should render correct accordions', () => { + const { getByText, getAllByText } = renderItemForm(); + + expect(getByText('Administrative data')).toBeInTheDocument(); + expect(getByText('Item data')).toBeInTheDocument(); + expect(getByText('Enumeration data')).toBeInTheDocument(); + expect(getByText('Condition')).toBeInTheDocument(); + expect(getByText('Item notes')).toBeInTheDocument(); + expect(getByText('Loan and availability')).toBeInTheDocument(); + expect(getByText('Location')).toBeInTheDocument(); + expect(getAllByText('Electronic access')[0]).toBeInTheDocument(); + expect(getAllByText('Bound-with and analytics')[0]).toBeInTheDocument(); }); }); diff --git a/src/edit/items/repeatableFields/BoundWithTitlesFields.js b/src/edit/items/repeatableFields/BoundWithTitlesFields.js new file mode 100644 index 000000000..35411b619 --- /dev/null +++ b/src/edit/items/repeatableFields/BoundWithTitlesFields.js @@ -0,0 +1,165 @@ +import React, { + useEffect, + useState, +} from 'react'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; +import { isEqual } from 'lodash'; +import { FieldArray } from 'react-final-form-arrays'; +import { Field } from 'react-final-form'; +import PropTypes from 'prop-types'; + +import { + Row, + Col, + Label, + TextField, + RepeatableField, + Button, + IconButton, +} from '@folio/stripes/components'; + +import BoundWithModal from '../BoundWithModal'; +import { useBoundWithTitlesByHrids } from '../../../hooks'; +import usePrevious from '../../../hooks/usePrevious'; + +const BoundWithTitlesFields = ({ + item, + addBoundWithTitles, +}) => { + const { formatMessage } = useIntl(); + + const [isBoundWithModalOpen, setBoundWithModalOpen] = useState(false); + const [addedHoldingsHrids, setAddedHoldingsHrids] = useState([]); + const { boundWithTitles: newBoundWithTitles } = useBoundWithTitlesByHrids(addedHoldingsHrids); + const prevBoundWithTitles = usePrevious(newBoundWithTitles); + + useEffect(() => { + if (!isEqual(prevBoundWithTitles, newBoundWithTitles)) { + addBoundWithTitles(newBoundWithTitles); + } + }, [newBoundWithTitles]); + + const hridLabel = formatMessage({ id: 'ui-inventory.instanceHrid' }); + const titleLabel = formatMessage({ id: 'ui-inventory.instanceTitleLabel' }); + const holdingsHridLabel = formatMessage({ id: 'ui-inventory.holdingsHrid' }); + const trashcanLabel = formatMessage({ id: 'stripes-components.deleteThisItem' }); + + const addBoundWiths = newHoldingsHrids => { + setAddedHoldingsHrids(newHoldingsHrids); + setBoundWithModalOpen(false); + }; + + const onBoundWithTitlesRemove = (fields, index) => { + const removedField = fields.value[index]; + const newlyAddedIndex = addedHoldingsHrids.indexOf(removedField.briefHoldingsRecord.hrid); + + if (newlyAddedIndex !== -1) { + const updatedHoldingsHrids = [...addedHoldingsHrids]; + updatedHoldingsHrids.splice(newlyAddedIndex, 1); + setAddedHoldingsHrids(updatedHoldingsHrids); + } + + fields.remove(index); + }; + + const headLabels = ( + + + + + + + + + + + + + + + ); + + const renderField = (field, index, fields) => ( + + + + + + + + + + + + onBoundWithTitlesRemove(fields, index)} + size="medium" + disabled={fields.value[index]?.briefHoldingsRecord?.id === item?.holdingsRecordId} + aria-label={trashcanLabel} + /> + + + ); + + return ( + <> + } + headLabels={headLabels} + renderField={renderField} + canAdd={false} + onRemove={false} + /> + + setBoundWithModalOpen(false)} + onOk={addBoundWiths} + /> + + ); +}; + +BoundWithTitlesFields.propTypes = { + item: PropTypes.object.isRequired, + addBoundWithTitles: PropTypes.func.isRequired, +}; + +export default BoundWithTitlesFields; diff --git a/src/edit/items/repeatableFields/CirculationNotesFields.js b/src/edit/items/repeatableFields/CirculationNotesFields.js new file mode 100644 index 000000000..015178794 --- /dev/null +++ b/src/edit/items/repeatableFields/CirculationNotesFields.js @@ -0,0 +1,124 @@ +import React from 'react'; +import { FieldArray } from 'react-final-form-arrays'; +import { Field } from 'react-final-form'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; +import PropTypes from 'prop-types'; + +import { + RepeatableField, + TextArea, + Select, + Checkbox, + Row, + Col, + Label, +} from '@folio/stripes/components'; + +const CirculationNotesFields = props => { + const { formatMessage } = useIntl(); + + const { + canAdd, + canEdit, + canDelete, + } = props; + + const noteTypeOptions = [ + { label: formatMessage({ id: 'ui-inventory.selectType' }), value: '' }, + { label: 'Check in note', value: 'Check in' }, + { label: 'Check out note', value: 'Check out' } + ]; + + const noteTypeLabel = formatMessage({ id: 'ui-inventory.noteType' }); + const noteLabel = formatMessage({ id: 'ui-inventory.note' }); + const staffOnlyLabel = formatMessage({ id: 'ui-inventory.staffOnly' }); + + const headLabels = ( + + + + + + + + + + + + ); + + const renderField = field => ( + + + + + + + + + + + + ); + + return ( + } + onAdd={fields => fields.push({ + noteType: '', + note: '', + staffOnly: false, + })} + headLabels={headLabels} + renderField={renderField} + canAdd={canAdd} + canRemove={canDelete} + /> + ); +}; + +CirculationNotesFields.propTypes = { + canAdd: PropTypes.bool, + canEdit: PropTypes.bool, + canDelete: PropTypes.bool, +}; +CirculationNotesFields.defaultProps = { + canAdd: true, + canEdit: true, + canDelete: true, +}; + +export default CirculationNotesFields; diff --git a/src/edit/items/repeatableFields/FormerIdentifierFields.js b/src/edit/items/repeatableFields/FormerIdentifierFields.js new file mode 100644 index 000000000..e878b70d7 --- /dev/null +++ b/src/edit/items/repeatableFields/FormerIdentifierFields.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; +import { FieldArray } from 'react-final-form-arrays'; +import { Field } from 'react-final-form'; +import PropTypes from 'prop-types'; + +import { + Label, + TextField, + RepeatableField, +} from '@folio/stripes/components'; + +const FormerIdentifierFields = ({ + canAdd, + canEdit, + canDelete, +}) => { + const { formatMessage } = useIntl(); + + const formerIdLabel = formatMessage({ id: 'ui-inventory.formerId' }); + + const legend = ( + + ); + + const renderField = field => ( + + ); + + return ( + } + onAdd={fields => fields.push('')} + headLabels={legend} + renderField={renderField} + canAdd={canAdd} + canRemove={canDelete} + /> + ); +}; + +FormerIdentifierFields.propTypes = { + canAdd: PropTypes.bool, + canEdit: PropTypes.bool, + canDelete: PropTypes.bool, +}; + +FormerIdentifierFields.defaultProps = { + canAdd: true, + canEdit: true, + canDelete: true, +}; + +export default FormerIdentifierFields; diff --git a/src/edit/items/repeatableFields/YearCaptionFields.js b/src/edit/items/repeatableFields/YearCaptionFields.js new file mode 100644 index 000000000..28db287fc --- /dev/null +++ b/src/edit/items/repeatableFields/YearCaptionFields.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; +import { FieldArray } from 'react-final-form-arrays'; +import { Field } from 'react-final-form'; +import PropTypes from 'prop-types'; + +import { + Label, + TextField, + RepeatableField, +} from '@folio/stripes/components'; + +const YearCaptionFields = ({ + canAdd, + canEdit, + canDelete, +}) => { + const { formatMessage } = useIntl(); + + const yearCaptionLabel = formatMessage({ id: 'ui-inventory.yearCaption' }); + + const legend = ( + + ); + + const renderField = field => ( + + ); + + return ( + } + onAdd={fields => fields.push('')} + headLabels={legend} + renderField={renderField} + canAdd={canAdd} + canRemove={canDelete} + /> + ); +}; + +YearCaptionFields.propTypes = { + canAdd: PropTypes.bool, + canEdit: PropTypes.bool, + canDelete: PropTypes.bool, +}; + +YearCaptionFields.defaultProps = { + canAdd: true, + canEdit: true, + canDelete: true, +}; + +export default YearCaptionFields; diff --git a/src/edit/items/repeatableFields/index.js b/src/edit/items/repeatableFields/index.js new file mode 100644 index 000000000..58e916d50 --- /dev/null +++ b/src/edit/items/repeatableFields/index.js @@ -0,0 +1,4 @@ +export { default as BoundWithTitlesFields } from './BoundWithTitlesFields'; +export { default as CirculationNotesFields } from './CirculationNotesFields'; +export { default as FormerIdentifierFields } from './FormerIdentifierFields'; +export { default as YearCaptionFields } from './YearCaptionFields'; diff --git a/src/edit/items/repeatableFields/tests/BoundWithTitlesFields.test.js b/src/edit/items/repeatableFields/tests/BoundWithTitlesFields.test.js new file mode 100644 index 000000000..0eb8ff7d4 --- /dev/null +++ b/src/edit/items/repeatableFields/tests/BoundWithTitlesFields.test.js @@ -0,0 +1,179 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/react'; + +import '../../../../../test/jest/__mock__'; + +import BoundWithTitlesFields from '../BoundWithTitlesFields'; +import useBoundWithTitlesByHrids from '../../../../hooks/useBoundWithTitlesByHrids'; + +import renderWithRouter from '../../../../../test/jest/helpers/renderWithRouter'; +import renderWithIntl from '../../../../../test/jest/helpers/renderWithIntl'; +import renderWithFinalForm from '../../../../../test/jest/helpers/renderWithFinalForm'; +import translationsProperties from '../../../../../test/jest/helpers/translationsProperties'; + +jest.mock('../../../../hooks/useBoundWithTitlesByHrids', () => { + return jest.fn(() => ({ isLoading: false, boundWithTitles: [] })); +}); +jest.mock('../../BoundWithModal', () => { + return jest.fn(({ open, onOk, onClose }) => (open ? ( +
+ BoundWith Modal + + +
+ ) : null)); +}); +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), + Loading: () =>
Loading
, +})); + +const addBoundWithTitlesProp = jest.fn(); +const initialValues = { + boundWithTitles: [ + { + briefInstance: { + title: 'instanceTitle', + hrid: 'instanceHrid' + }, + briefHoldingsRecord: { + id: 'holdingsId', + hrid: 'holdingHrid', + }, + }, + ], +}; + +const renderBoundWithTitlesFields = (props = {}) => { + const component = ( + + ); + + return renderWithIntl( + renderWithRouter(renderWithFinalForm(component, { initialValues })), + translationsProperties, + ); +}; + +describe('BoundWithTitlesFields', () => { + it('should render a legend', () => { + const { getByText } = renderBoundWithTitlesFields(); + + expect(getByText('Bound-with and analytics')).toBeInTheDocument(); + }); + + it('should render Add Bound-with and analytics button', () => { + const { getByText } = renderBoundWithTitlesFields(); + + expect(getByText('Add Bound-with and analytics')).toBeInTheDocument(); + }); + + describe('when there is data', () => { + it('should render correct fields', () => { + const { getByLabelText } = renderBoundWithTitlesFields(); + + expect(getByLabelText('Instance HRID')).toBeInTheDocument(); + expect(getByLabelText('Instance title')).toBeInTheDocument(); + expect(getByLabelText('Holdings HRID')).toBeInTheDocument(); + }); + + it('all fields should be disabled', () => { + const { getByLabelText } = renderBoundWithTitlesFields(); + + expect(getByLabelText('Instance HRID')).toBeDisabled(); + expect(getByLabelText('Instance title')).toBeDisabled(); + expect(getByLabelText('Holdings HRID')).toBeDisabled(); + }); + }); + + describe('when clicking Add Bound-with and analytics button', () => { + it('BoundWith Modal should appear', async () => { + const { getByText } = renderBoundWithTitlesFields(); + const addButton = getByText('Add Bound-with and analytics'); + userEvent.click(addButton); + + await waitFor(() => expect(getByText('BoundWith Modal')).toBeInTheDocument()); + }); + + describe('when clicking Cancel button', () => { + it('BoundWith Modal should disappear', async () => { + const { queryByText, getByRole, getByText } = renderBoundWithTitlesFields(); + const addButton = getByText('Add Bound-with and analytics'); + userEvent.click(addButton); + userEvent.click(getByRole('button', { name: 'Cancel' })); + + await waitFor(() => expect(queryByText('BoundWith Modal')).not.toBeInTheDocument()); + }); + }); + + describe('when clicking Save & close button', () => { + it('BoundWith Modal should disappear', async () => { + const { queryByText, getByRole, getByText } = renderBoundWithTitlesFields(); + const addButton = getByText('Add Bound-with and analytics'); + userEvent.click(addButton); + userEvent.click(getByRole('button', { name: 'Save & close' })); + + await waitFor(() => expect(queryByText('BoundWith Modal')).not.toBeInTheDocument()); + }); + + it('and new bound with titles info should be fetched', async () => { + const { getByRole, getByText } = renderBoundWithTitlesFields(); + const addButton = getByText('Add Bound-with and analytics'); + userEvent.click(addButton); + userEvent.click(getByRole('button', { name: 'Save & close' })); + + expect(useBoundWithTitlesByHrids).toHaveBeenCalled(); + }); + }); + }); + + describe("when item's holdingsRecordId is equal to added briefHoldingsRecord id", () => { + it('trash icon for this field should be disabled', () => { + const { getByRole } = renderBoundWithTitlesFields({ item: { holdingsRecordId: 'holdingsId' } }); + + const deleteButton = getByRole('button', { name: /delete this item/i }); + + expect(deleteButton).toBeDisabled(); + }); + }); + + describe("when item's holdingsRecordId is not equal to added briefHoldingsRecord id", () => { + it('trash icon for this field should be active', () => { + const { getByRole } = renderBoundWithTitlesFields({ item: { holdingsRecordId: 'holdingsId1' } }); + + const deleteButton = getByRole('button', { name: /delete this item/i }); + + expect(deleteButton).not.toBeDisabled(); + }); + }); + + describe('when clicking trash icon', () => { + it('should remove repeatable field', () => { + const { queryByLabelText, getByRole } = renderBoundWithTitlesFields(); + + const deleteButton = getByRole('button', { name: /delete this item/i }); + + userEvent.click(deleteButton); + + expect(queryByLabelText('Instance HRID')).not.toBeInTheDocument(); + expect(queryByLabelText('Instance title')).not.toBeInTheDocument(); + expect(queryByLabelText('Holdings HRID')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/edit/noteFields.js b/src/edit/noteFields.js index d1759b355..2cf47c783 100644 --- a/src/edit/noteFields.js +++ b/src/edit/noteFields.js @@ -21,35 +21,37 @@ const NoteFields = props => { const { formatMessage } = useIntl(); const { - instanceNoteTypes, + noteTypeOptions, + noteTypeIdField, + requiredFields, + renderLegend, canAdd, canEdit, canDelete, } = props; - const instanceNoteTypeOptions = instanceNoteTypes.map(it => ({ - label: it.name, - value: it.id, - })); - const noteTypeLabel = formatMessage({ id: 'ui-inventory.noteType' }); const noteLabel = formatMessage({ id: 'ui-inventory.note' }); const staffOnlyLabel = formatMessage({ id: 'ui-inventory.staffOnly' }); + const isNoteTypeRequired = requiredFields.some(field => field === noteTypeIdField); + const isNoteRequired = requiredFields.some(field => field === 'note'); + const isStaffOnlyRequired = requiredFields.some(field => field === 'staffOnly'); + const headLabels = ( - @@ -94,10 +99,10 @@ const NoteFields = props => { } + legend={renderLegend ? : null} addLabel={} onAdd={fields => fields.push({ - instanceNoteTypeId: '', + [noteTypeIdField]: '', note: '', staffOnly: false, })} @@ -110,12 +115,18 @@ const NoteFields = props => { }; NoteFields.propTypes = { - instanceNoteTypes: PropTypes.arrayOf(PropTypes.object), + noteTypeIdField: PropTypes.string.isRequired, + noteTypeOptions: PropTypes.arrayOf(PropTypes.object), + requiredFields: PropTypes.arrayOf(PropTypes.string), + renderLegend: PropTypes.bool, canAdd: PropTypes.bool, canEdit: PropTypes.bool, canDelete: PropTypes.bool, }; NoteFields.defaultProps = { + noteTypeOptions: [], + requiredFields: [], + renderLegend: true, canAdd: true, canEdit: true, canDelete: true, diff --git a/src/edit/noteFields.test.js b/src/edit/noteFields.test.js index 74be32f07..77e86db6d 100644 --- a/src/edit/noteFields.test.js +++ b/src/edit/noteFields.test.js @@ -16,7 +16,8 @@ jest.unmock('@folio/stripes/components'); const onSubmit = jest.fn(); const props = { - instanceNoteTypes: [{ name: 'instanceNoteTypesName', id: '123456789' }] + noteTypeIdField: 'noteTypeIdField', + noteTypeOptions: [{ name: 'noteTypeName', id: '123456789' }], }; const Form = ({ handleSubmit }) => ( @@ -43,15 +44,26 @@ const renderNoteFields = () => renderWithIntl( afterEach(() => jest.clearAllMocks()); describe('NoteFields', () => { - it('renders RepeatableField', () => { + it('should render a legend', () => { renderNoteFields(); + expect(screen.getByText('Notes')).toBeInTheDocument(); }); - it('click on Add note button', () => { - renderNoteFields(); - const addNoteButton = screen.getByText('Add note'); - userEvent.click(addNoteButton); - const RelationshipDropdown = screen.getAllByText('instanceNoteTypesName'); - expect(RelationshipDropdown).toHaveLength(1); + + describe('when clicking on Add note button', () => { + it('correct fields should be rendered', () => { + renderNoteFields(); + + const addNoteButton = screen.getByText('Add note'); + userEvent.click(addNoteButton); + + const relationshipDropdown = screen.getAllByRole('option'); + + // where 1 of options is a default option + expect(relationshipDropdown).toHaveLength(2); + expect(screen.getAllByText(/Note type/i)[0]).toBeInTheDocument(); + expect(screen.getAllByText(/Note/i)[0]).toBeInTheDocument(); + expect(screen.getAllByText(/Staff only/i)[0]).toBeInTheDocument(); + }); }); }); diff --git a/src/hooks/index.js b/src/hooks/index.js index 2ccfdb29a..1842bb751 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1,3 +1,4 @@ +export { default as useBoundWithTitlesByHrids } from './useBoundWithTitlesByHrids'; export { default as useBrowseValidation } from './useBrowseValidation'; export { default as useCallout } from './useCallout'; export { default as useHoldingItemsQuery } from './useHoldingItemsQuery'; diff --git a/src/hooks/useBoundWithTitlesByHrids/index.js b/src/hooks/useBoundWithTitlesByHrids/index.js new file mode 100644 index 000000000..e5b4c1cd1 --- /dev/null +++ b/src/hooks/useBoundWithTitlesByHrids/index.js @@ -0,0 +1 @@ +export { default } from './useBoundWithTitlesByHrids'; diff --git a/src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.js b/src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.js new file mode 100644 index 000000000..ba114a000 --- /dev/null +++ b/src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.js @@ -0,0 +1,48 @@ +import { keyBy } from 'lodash'; + +import useHoldingsQueryByHrids from '../useHoldingsQueryByHrids'; +import useInstancesQuery from '../useInstancesQuery'; + +const useBoundWithTitlesByHrids = holdingsHrids => { + // Load the holdings records matching the input HRIDs + const { isLoading: isHoldingsLoading, holdingsRecords } = useHoldingsQueryByHrids(holdingsHrids); + + // Load the instance records matching the holdings' instanceIds. + const instanceIds = holdingsRecords.map(record => record.instanceId); + const instances = useInstancesQuery(instanceIds); + + if (isHoldingsLoading || !instances.isSuccess) return { isLoading: true, boundWithTitles: [] }; + + const holdingsRecordsByHrid = keyBy(holdingsRecords, 'hrid'); + const instancesById = keyBy(instances.data?.instances, 'id'); + + const boundWithTitles = holdingsHrids.map(hrid => { + let boundWithTitle = { briefHoldingsRecord: { hrid } }; + const holdingsRecord = holdingsRecordsByHrid[hrid]; + + if (holdingsRecord) { + const instance = instancesById[holdingsRecord.instanceId]; + + boundWithTitle = { + briefHoldingsRecord: { + hrid, + id: holdingsRecord.id, + }, + briefInstance: { + id: instance?.id, + hrid: instance?.hrid, + title: instance?.title, + }, + }; + } + + return boundWithTitle; + }); + + return { + isLoading: false, + boundWithTitles, + }; +}; + +export default useBoundWithTitlesByHrids; diff --git a/src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.test.js b/src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.test.js new file mode 100644 index 000000000..fa90e8cdc --- /dev/null +++ b/src/hooks/useBoundWithTitlesByHrids/useBoundWithTitlesByHrids.test.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; + +import '../../../test/jest/__mock__'; + +import useBoundWithTitlesByHrids from './useBoundWithTitlesByHrids'; + +jest.mock('../useHoldingsQueryByHrids', () => { + const mockHoldingsRecords = { + isLoading: false, + holdingsRecords: [{ + instanceId: 'instanceId', + hrid: 'holdingHrid', + id: 'holdingId', + }], + }; + + return () => mockHoldingsRecords; +}); +jest.mock('../useInstancesQuery', () => { + const mockInstances = { + isSuccess: true, + data: { instances: [ + { + id: 'instanceId', + hrid: 'instanceHrid', + title: 'instanceTitle', + }, + ] }, + }; + + return () => mockInstances; +}); + +describe('useBoundWithTitlesByHrids', () => { + it('should return boundWithTitles data', async () => { + const { result, waitFor } = renderHook(() => useBoundWithTitlesByHrids(['holdingHrid'])); + + await waitFor(() => !result.current.isLoading); + + const expectedResult = [{ + briefHoldingsRecord: { + hrid: 'holdingHrid', + id: 'holdingId', + }, + briefInstance: { + id: 'instanceId', + hrid: 'instanceHrid', + title: 'instanceTitle', + }, + }]; + + expect(result.current.boundWithTitles).toEqual(expectedResult); + }); +}); + diff --git a/test/jest/helpers/index.js b/test/jest/helpers/index.js index 19131be08..b9c939cf3 100644 --- a/test/jest/helpers/index.js +++ b/test/jest/helpers/index.js @@ -1,3 +1,4 @@ export { default as stripesStub } from './stripesStub'; export { default as translationsProperties } from './translationsProperties'; +export { default as renderWithFinalForm } from './renderWithFinalForm'; export { default as renderWithIntl } from './renderWithIntl'; diff --git a/test/jest/helpers/renderWithFinalForm.js b/test/jest/helpers/renderWithFinalForm.js new file mode 100644 index 000000000..ca7bee3ec --- /dev/null +++ b/test/jest/helpers/renderWithFinalForm.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import stripesFinalForm from '@folio/stripes/final-form'; + +const renderWithFinalForm = ( + component, + { + onSubmit: onCustomSubmit, + initialValues = {}, + navigationCheck = true, + enableReinitialize = false, + ...stripesFinalFormProps + } = {} +) => { + const onSubmit = () => {}; + + const Form = ({ handleSubmit }) => ( +
+ {component} +
+ ); + + const WrappedForm = stripesFinalForm({ + navigationCheck, + enableReinitialize, + ...stripesFinalFormProps + })(Form); + + return ( + + ); +}; + +export default renderWithFinalForm; From 8e86491d0ae1b62584cbb955d90909dbfc33a180 Mon Sep 17 00:00:00 2001 From: VSnehalatha <53073086+VSnehalatha@users.noreply.github.com> Date: Tue, 9 May 2023 19:11:39 +0530 Subject: [PATCH 11/68] UIIN-1708 Jest/RTL test for useReferenceData.js (#2105) --- src/hooks/useReferenceData.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/hooks/useReferenceData.test.js diff --git a/src/hooks/useReferenceData.test.js b/src/hooks/useReferenceData.test.js new file mode 100644 index 000000000..1fa3afb30 --- /dev/null +++ b/src/hooks/useReferenceData.test.js @@ -0,0 +1,14 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { DataContext } from '../contexts'; +import useReferenceData from './useReferenceData'; + +describe('useReferenceData', () => { + it('return the data from the DataContext', () => { + const data = { foo: 'dataFoo' }; + const wrapper = ({ children }) => ( + {children} + ); + const { result } = renderHook(() => useReferenceData(), { wrapper }); + expect(result.current).toEqual(data); + }); +}); From a2018d72ce8972d8b437e53a255d2adf9a625833 Mon Sep 17 00:00:00 2001 From: VSnehalatha <53073086+VSnehalatha@users.noreply.github.com> Date: Tue, 9 May 2023 19:30:12 +0530 Subject: [PATCH 12/68] UIIN-1721 create Jest/RTL test for InstanceDetails.js (#2118) --- .../InstanceDetails/InstanceDetails.test.js | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 src/Instance/InstanceDetails/InstanceDetails.test.js diff --git a/src/Instance/InstanceDetails/InstanceDetails.test.js b/src/Instance/InstanceDetails/InstanceDetails.test.js new file mode 100644 index 000000000..225ce1ec8 --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceDetails.test.js @@ -0,0 +1,240 @@ +import React from 'react'; +import '../../../test/jest/__mock__'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; +import { DataContext } from '../../contexts'; +import { renderWithIntl, translationsProperties } from '../../../test/jest/helpers'; +import InstanceDetails from './InstanceDetails'; + +jest.mock('../../components/ViewSource/ViewSource', () => jest.fn().mockReturnValue('ViewSource')); +jest.mock('../InstanceDetails/InstanceTitle/InstanceTitle', () => jest.fn().mockReturnValue('InstanceTitle')); +jest.mock('../InstanceDetails/InstanceContributorsView/InstanceContributorsView', () => jest.fn().mockReturnValue('InstanceContributorsView')); +jest.mock('../InstanceDetails/InstanceSubjectView/InstanceSubjectView', () => jest.fn().mockReturnValue('InstanceSubjectView')); +jest.mock('../InstanceDetails/InstanceTitleData/AlternativeTitlesList', () => jest.fn().mockReturnValue('AlternativeTitlesList')); +jest.mock('../InstanceDetails/InstanceTitleData/TitleSeriesStatements', () => jest.fn().mockReturnValue('TitleSeriesStatements')); +jest.mock('../InstanceDetails/ControllableDetail/ControllableDetail', () => jest.fn().mockReturnValue('ControllableDetail')); +jest.mock('../InstanceDetails/SubInstanceGroup/SubInstanceGroup', () => jest.fn().mockReturnValue('SubInstanceGroup')); +jest.mock('../InstanceDetails/InstanceAcquisition/InstanceAcquisition', () => jest.fn().mockReturnValue('InstanceAcquisition')); +jest.mock('../InstanceDetails/InstanceTitleData/InstanceTitleData', () => jest.fn().mockReturnValue('InstanceTitleData')); + +jest.mock('@folio/stripes-core', () => ({ + ...jest.requireActual('@folio/stripes-core'), + TitleManager: ({ children }) => <>{children} +})); + +const instance = { + title: 'Test Title', + contributors: [], + identifiers: [], + instanceTypeId: '1234', + instanceFormatIds: [], + physicalDescriptions: [], + languages: [], + publication: [], + notes: [], + staffSuppress: false, + discoverySuppress: false, +}; + +const mockReferenceData = { + titleTypes:[ + { id: '1', name: 'Type 1' }, + { id: '2', name: 'Type 2' }, + ], + instanceTypes: [ + { id: '1', name: 'Book' }, + { id: '2', name: 'E-book' }, + ] +}; + +const queryClient = new QueryClient(); + +const actionMenu = jest.fn(); +const onClose = jest.fn(); +const tagsEnabled = true; +describe('InstanceDetails', () => { + it('renders the InstanceDetails component', () => { + renderWithIntl( + + + + , + + + , + translationsProperties + ); + expect(screen.getByText('InstanceTitle')).toBeInTheDocument(); + expect(screen.getByText('Add holdings')).toBeInTheDocument(); + expect(screen.getByText('Administrative data')).toBeInTheDocument(); + expect(screen.getByText('Instance HRID')).toBeInTheDocument(); + expect(screen.getByText('Source')).toBeInTheDocument(); + expect(screen.getByText('Cataloged date')).toBeInTheDocument(); + expect(screen.getByText('Instance status term')).toBeInTheDocument(); + expect(screen.getByText('status updated -')).toBeInTheDocument(); + expect(screen.getByText('Instance status code')).toBeInTheDocument(); + expect(screen.getByText('Instance status source')).toBeInTheDocument(); + expect(screen.getByText('Mode of issuance')).toBeInTheDocument(); + expect(screen.getByText('Statistical code type')).toBeInTheDocument(); + expect(screen.getByText('Statistical code')).toBeInTheDocument(); + expect(screen.getByText('Format category')).toBeInTheDocument(); + expect(screen.getByText('Format term')).toBeInTheDocument(); + expect(screen.getByText('Format code')).toBeInTheDocument(); + expect(screen.getByText('Format source')).toBeInTheDocument(); + expect(screen.getByText('Language')).toBeInTheDocument(); + expect(screen.getByText('Publication frequency')).toBeInTheDocument(); + expect(screen.getByText('Publication range')).toBeInTheDocument(); + expect(screen.getByText('Instance notes')).toBeInTheDocument(); + expect(screen.getByText('Staff only')).toBeInTheDocument(); + expect(screen.getByText('Note')).toBeInTheDocument(); + expect(screen.getByText('Electronic access')).toBeInTheDocument(); + expect(screen.getByText('URL relationship')).toBeInTheDocument(); + expect(screen.getByText('URI')).toBeInTheDocument(); + expect(screen.getByText('Link text')).toBeInTheDocument(); + expect(screen.getByText('Materials specified')).toBeInTheDocument(); + expect(screen.getByText('URL public note')).toBeInTheDocument(); + expect(screen.getByText('Classification identifier type')).toBeInTheDocument(); + expect(screen.getByText('Instance relationship (analytics and bound-with)')).toBeInTheDocument(); + }); + + it('should show a correct Warning message banner when staff suppressed', () => { + const staffSuppressedInstance = { + ...instance, + staffSuppress: true, + }; + renderWithIntl( + + + + , + + + , + translationsProperties + ); + + expect(screen.getByText('Warning: Instance is marked staff suppressed')).toBeInTheDocument(); + expect(screen.getByText('Staff suppressed')).toBeInTheDocument(); + }); + + it('should show a correct Warning message banner when discovery suppressed', () => { + const discoverySuppressedInstance = { + ...instance, + discoverySuppress: true, + }; + renderWithIntl( + + + + , + + + , + translationsProperties + ); + + expect(screen.getByText('Warning: Instance is marked suppressed from discovery')).toBeInTheDocument(); + expect(screen.getByText('Suppressed from discovery')).toBeInTheDocument(); + }); + it('should show a correct Warning message banner when both staff and discovery suppressed', () => { + const bothSuppressedInstance = { + ...instance, + staffSuppress: true, + discoverySuppress: true, + }; + renderWithIntl( + + + + , + + + , + translationsProperties + ); + + expect(screen.getByText('Warning: Instance is marked suppressed from discovery and staff suppressed')).toBeInTheDocument(); + }); + + it('expands and collapses the accordion sections', () => { + renderWithIntl( + + + + , + + + , + translationsProperties + ); + + const expandAllButtons = screen.getByText('Expand all'); + const firstAccordionSection = screen.getByRole('button', { name: /Administrative data/i }); + const secondAccordionSection = screen.getByRole('button', { name: /Instance notes/i }); + const thirdAccordionSection = screen.getByRole('button', { name: /Electronic access/i }); + const fourthAccordionSection = screen.getByRole('button', { name: /Classification/i }); + expect(firstAccordionSection.getAttribute('aria-expanded')).toBe('false'); + expect(secondAccordionSection.getAttribute('aria-expanded')).toBe('false'); + expect(thirdAccordionSection.getAttribute('aria-expanded')).toBe('false'); + expect(fourthAccordionSection.getAttribute('aria-expanded')).toBe('false'); + userEvent.click(expandAllButtons); + expect(firstAccordionSection.getAttribute('aria-expanded')).toBe('true'); + expect(secondAccordionSection.getAttribute('aria-expanded')).toBe('true'); + expect(thirdAccordionSection.getAttribute('aria-expanded')).toBe('true'); + expect(fourthAccordionSection.getAttribute('aria-expanded')).toBe('true'); + const collapseAllButtons = screen.getByText('Collapse all'); + userEvent.click(collapseAllButtons); + expect(firstAccordionSection.getAttribute('aria-expanded')).toBe('false'); + expect(secondAccordionSection.getAttribute('aria-expanded')).toBe('false'); + expect(thirdAccordionSection.getAttribute('aria-expanded')).toBe('false'); + expect(fourthAccordionSection.getAttribute('aria-expanded')).toBe('false'); + }); + + it('renders tags button if tagsEnabled is true', () => { + renderWithIntl( + + + + , + + + , + translationsProperties + ); + const button = screen.getAllByRole('button', { id: 'clickable-show-tags' }); + userEvent.click(button[1]); + expect(button[1]).toBeEnabled(); + }); +}); From 6365cec7e21a334bbe0d8fed3046c21431a9dfa9 Mon Sep 17 00:00:00 2001 From: ssandupatla <58558770+ssandupatla@users.noreply.github.com> Date: Tue, 9 May 2023 21:53:36 +0530 Subject: [PATCH 13/68] UIIN-1684 create JEST/RTL for Check.js (#2080) * UIIN-1684 create JEST/RTL for Check.js * Update Check.test.js Check.test.js Fille updated --- src/RemoteStorageService/Check.test.js | 57 ++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/RemoteStorageService/Check.test.js diff --git a/src/RemoteStorageService/Check.test.js b/src/RemoteStorageService/Check.test.js new file mode 100644 index 000000000..b6a832488 --- /dev/null +++ b/src/RemoteStorageService/Check.test.js @@ -0,0 +1,57 @@ +import { renderHook } from '@testing-library/react-hooks'; +import '../../test/jest/__mock__'; +import { useHoldings } from '../providers'; +import { useByLocation, useByHoldings } from './Check'; + +jest.mock('../providers', () => ({ + useHoldings: jest.fn(), +})); + +describe('useByLocation', () => { + it('return true if fromLocationId is in remoteMap and toLocationId is not', () => { + const { result } = renderHook(() => useByLocation()); + expect(result.current({ fromLocationId: 'holdings-id-1', toLocationId: 'holdings-id-3' })).toBe(true); + }); + it('return false if fromLocationId is not in remoteMap', () => { + const { result } = renderHook(() => useByLocation()); + expect(result.current({ fromLocationId: 'holdings-id-4', toLocationId: 'holdings-id-2' })).toBe(false); + }); + it('return false if toLocationId is in remoteMap', () => { + const { result } = renderHook(() => useByLocation()); + expect(result.current({ fromLocationId: 'holdings-id-1', toLocationId: 'holdings-id-2' })).toBe(false); + }); +}); + +describe('useByHoldings', () => { + beforeEach(() => { + useHoldings.mockReturnValue({ holdingsById: { + 'holdings-id-1': { permanentLocationId: 'holdings-id-1' }, + 'holdings-id-2': { permanentLocationId: 'holdings-id-2' }, + } }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('return true if from location is in remote storage and to location is not', () => { + const { result } = renderHook(() => useByHoldings()); + expect(result.current({ fromHoldingsId: 'holdings-id-1', toHoldingsId: 'holdings-id-3' })).toBe(true); + }); + it('return false if holdingsById is undefined', () => { + useHoldings.mockReturnValueOnce({ holdingsById: undefined }); + const { result } = renderHook(() => useByHoldings()); + expect(result.current({ fromHoldingsId: 'holdings-id-1', toHoldingsId: 'holdings-id-2' })).toBe(false); + }); + it('return false if from location is not in remote storage', () => { + const { result } = renderHook(() => useByHoldings()); + expect(result.current({ fromHoldingsId: 'holdings-id-3', toHoldingsId: 'holdings-id-2' })).toBe(false); + }); + it('return false if to location is in remote storage', () => { + useHoldings.mockReturnValueOnce({ holdingsById: { + 'holdings-id-1': { permanentLocationId: 'holdings-id-1' }, + 'holdings-id-2': { permanentLocationId: 'holdings-id-2' }, + } }); + const { result } = renderHook(() => useByHoldings()); + expect(result.current({ fromHoldingsId: 'holdings-id-1', toHoldingsId: 'holdings-id-2' })).toBe(false); + }); +}); + From d83437d64b1bb52f9360b542b3e0533ab6b866e3 Mon Sep 17 00:00:00 2001 From: VSnehalatha <53073086+VSnehalatha@users.noreply.github.com> Date: Tue, 9 May 2023 22:11:37 +0530 Subject: [PATCH 14/68] UIIN-1764 Jest/RTL test for ItemStatus.js (#2112) --- src/views/ItemStatus/ItemStatus.test.js | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/views/ItemStatus/ItemStatus.test.js diff --git a/src/views/ItemStatus/ItemStatus.test.js b/src/views/ItemStatus/ItemStatus.test.js new file mode 100644 index 000000000..6a1b048a7 --- /dev/null +++ b/src/views/ItemStatus/ItemStatus.test.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { screen } from '@testing-library/react'; +import { act } from '@testing-library/react-hooks'; + +import '../../../test/jest/__mock__'; + +import { + renderWithIntl, + translationsProperties, +} from '../../../test/jest/helpers'; + +import { + itemStatuses, +} from '../../constants'; + +import ItemStatus from './ItemStatus'; + +jest.mock('../../constants', () => ({ + ...jest.requireActual('../../constants'), + itemStatusesMap: () =>
ViewMetaData 1
, +})); + +const mockMutator = { + item: { + GET: jest.fn().mockResolvedValueOnce([{ id: 123, name: 'Test' }]), + }, + servicePoint: { + GET: jest.fn().mockResolvedValueOnce([{ id: 223, name: 'Test2' }]), + }, +}; + +const defaultProps = { + itemId: 'item-123', + status: { name: 'Available', date: '2022-01-01T00:00:00.000Z' }, + openLoan: {}, + mutator: mockMutator, +}; + +const statusData = { name: 'Checked out', date: '2023-03-01T00:00:00.000Z' }; + +const ItemStatusSetup = ({ status }) => ( + + + +); + +const renderItemStatusSetup = (status = statusData) => renderWithIntl( + , + translationsProperties +); + +describe('ItemStatus', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('render ItemStatus', () => { + renderItemStatusSetup(); + expect(screen.getByText(/Item status/i)).toBeInTheDocument(); + expect(screen.getByText(/Checked out/i)).toBeInTheDocument(); + expect(screen.getByText('status updated 3/1/2023, 12:00 AM')).toBeInTheDocument(); + act(() => { + renderItemStatusSetup(defaultProps, itemStatuses); + expect(screen.getByText(/Loading/i)).toBeInTheDocument(); + }); + }); +}); From 0f02281f082fcb836f0a0b7624ef84decb5b14a4 Mon Sep 17 00:00:00 2001 From: ssandupatla <58558770+ssandupatla@users.noreply.github.com> Date: Tue, 9 May 2023 22:30:23 +0530 Subject: [PATCH 15/68] UIIN-1779 create Jest/RTL test for ViewInstance.js (#2144) --- src/ViewInstance.test.js | 317 ++++++++++++++++--- test/jest/__mock__/stripesComponents.mock.js | 16 +- 2 files changed, 288 insertions(+), 45 deletions(-) diff --git a/src/ViewInstance.test.js b/src/ViewInstance.test.js index 27b89af18..0d1de999b 100644 --- a/src/ViewInstance.test.js +++ b/src/ViewInstance.test.js @@ -1,64 +1,161 @@ -import { screen } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from 'react-query'; -import { MemoryRouter } from 'react-router-dom'; - import '../test/jest/__mock__'; - +import React from 'react'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { instances } from '../test/fixtures/instances'; -import { renderWithIntl, translationsProperties } from '../test/jest/helpers'; import { DataContext } from './contexts'; import StripesConnectedInstance from './ConnectedInstance/StripesConnectedInstance'; +import { renderWithIntl, translationsProperties } from '../test/jest/helpers'; import ViewInstance from './ViewInstance'; +const spyOncollapseAllSections = jest.spyOn(require('@folio/stripes/components'), 'collapseAllSections'); + +const spyOnexpandAllSections = jest.spyOn(require('@folio/stripes/components'), 'expandAllSections'); + +jest.mock('@folio/stripes-core', () => ({ + ...jest.requireActual('@folio/stripes-core'), + TitleManager: ({ children }) => <>{children} +})); + +jest.mock('./components/ImportRecordModal/ImportRecordModal', () => (props) => { + const { isOpen, handleSubmit, handleCancel } = props; + if (isOpen) { + const args = { + externalIdentifierType: 'typeTest', + externalIdentifier: 'externalIdentifier', + selectedJobProfileId: 'profileId' + }; + const container = +
+
ImportRecordModal
+ + +
; + return container; + } + return null; +}); + +jest.mock('./components/InstancePlugin/InstancePlugin', () => ({ onSelect, onClose }) => { + return ( +
+
InstancePlugin
+ + +
+ ); +}); + jest.mock('./RemoteStorageService/Check', () => ({ ...jest.requireActual('./RemoteStorageService/Check'), useByLocation: jest.fn(() => false), useByHoldings: jest.fn(() => false), })); +const location = { + pathname: '/testPathName', + search: '?filters=test1&query=test2&sort=test3&qindex=test', + hash: '' +}; +const mockPush = jest.fn(); +const mockReplace = jest.fn(); +const history = createMemoryHistory(); +history.location = location; +history.push = mockPush; +history.replace = mockReplace; const instance = { ...instances[0], + subjects:['Information society--Political aspects'], parentInstances: [], childInstances: [], }; jest .spyOn(StripesConnectedInstance.prototype, 'instance') - .mockImplementation(() => instance); + .mockImplementation(() => instance) + .mockImplementationOnce(() => {}); -const defaultProps = { +const goToMock = jest.fn(); +const mockReset = jest.fn(); +const updateMock = jest.fn(); +const mockonClose = jest.fn(); +const mockData = jest.fn().mockResolvedValue(true); +const defaultProp = { selectedInstance: instance, - paneWidth: '44%', - openedFromBrowse: false, - onCopy: jest.fn(), - onClose: jest.fn(), + goTo: goToMock, + match: { + path: '/inventory/view', + params: { + id: 'testId' + }, + }, + intl: { + formatMessage: jest.fn(), + }, mutator: { allInstanceItems: { - GET: jest.fn(() => Promise.resolve([])), - reset: jest.fn(), - }, - configs: { - GET: jest.fn(() => Promise.resolve([])), - reset: jest.fn(), + reset: mockReset }, holdings: { - POST: jest.fn(() => Promise.resolve({})), - reset: jest.fn(), - }, - instanceRequests: { - GET: jest.fn(() => Promise.resolve({})), - reset: jest.fn(), + POST: jest.fn(), }, marcRecord: { - GET: jest.fn(() => Promise.resolve([])), - reset: jest.fn(), + GET: mockData, + }, + query: { + update: updateMock, }, movableItems: { GET: jest.fn(() => Promise.resolve([])), reset: jest.fn(), }, - query: {}, + instanceRequests: { + GET: jest.fn(() => Promise.resolve([])), + reset: jest.fn() + } + }, + onClose: mockonClose, + onCopy: jest.fn(), + openedFromBrowse: false, + paneWidth: '55%', + resources: { + allInstanceItems: { + reset: mockReset + }, + allInstanceHoldings: {}, + locations: {}, + configs: { + hasLoaded: true, + records: [ + { + value: 'testParse' + }, + ] + }, + instanceRequests: { + other: { + totalRecords: 10, + }, + records: [ + { + id: 'testIdRecord1' + } + ], + }, }, + stripes: { + connect: jest.fn(), + hasPerm: jest.fn().mockReturnValue(true), + locale: 'Testlocale', + logger: { + log: jest.fn() + }, + }, + tagsEnabled: true, + updateLocation: jest.fn(), }; const referenceData = { @@ -80,40 +177,174 @@ const queryClient = new QueryClient(); const renderViewInstance = (props = {}) => renderWithIntl( - - + , translationsProperties ); describe('ViewInstance', () => { beforeEach(() => { - // defaultProps.history.push.mockClear(); + jest.clearAllMocks(); }); - - describe('Action menu', () => { - it('should display action menu items', async () => { + it('should display action menu items', () => { + renderViewInstance(); + expect(screen.getByText('Move items within an instance')).toBeInTheDocument(); + expect(screen.getByText('Move holdings/items to another instance')).toBeInTheDocument(); + }); + it('should NOT display \'move\' action menu items when instance was opened from Browse page', () => { + renderViewInstance({ openedFromBrowse: true }); + expect(screen.queryByText('Move items within an instance')).not.toBeInTheDocument(); + expect(screen.queryByText('Move holdings/items to another instance')).not.toBeInTheDocument(); + }); + describe('Action Menu', () => { + it('"onClickEditInstance" should be called when the user clicks the "Edit instance" button', () => { renderViewInstance(); - - expect(screen.getByText('Move items within an instance')).toBeInTheDocument(); - expect(screen.getByText('Move holdings/items to another instance')).toBeInTheDocument(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Edit instance' })); + expect(mockPush).toBeCalled(); }); - - it('should NOT display \'move\' action menu items when instance was opened from Browse page', async () => { - renderViewInstance({ openedFromBrowse: true }); - - expect(screen.queryByText('Move items within an instance')).not.toBeInTheDocument(); - expect(screen.queryByText('Move holdings/items to another instance')).not.toBeInTheDocument(); + it('"onClickViewRequests" should be called when the user clicks the "View requests" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'View requests' })); + expect(mockPush).toBeCalled(); + }); + it('"onCopy" function should be called when the user clicks the "Duplicate instance" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Duplicate instance' })); + expect(defaultProp.onCopy).toBeCalled(); + }); + it('"handleViewSource" should be called when the user clicks the "View source" button', async () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + const veiwSourceButton = screen.getByRole('button', { name: 'View source' }); + await waitFor(() => { + expect(veiwSourceButton).not.toHaveAttribute('disabled'); + }); + userEvent.click(veiwSourceButton); + expect(goToMock).toBeCalled(); + }); + it('"createHoldingsMarc" should be called when the user clicks the "Add MARC holdings record" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Add MARC holdings record' })); + expect(mockPush).toBeCalled(); + }); + it('"Move items within an instance" button to be clicked', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Move items within an instance' })); + expect(renderViewInstance()).toBeTruthy(); + }); + it('"InstancePlugin" should render when user clicks "Move holdings/items to another instance" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Move holdings/items to another instance' })); + expect(screen.getByRole('button', { name: '+' })); + }); + it('"ImportRecordModal" component should render when user clicks "Overlay source bibliographic record" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Overlay source bibliographic record' })); + expect(screen.getByText('ImportRecordModal')).toBeInTheDocument(); + }); + it('"handleImportRecordModalSubmit" should be called when the user clicks the "handleSubmit" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Overlay source bibliographic record' })); + userEvent.click(screen.getByRole('button', { name: 'handleSubmit' })); + expect(updateMock).toBeCalled(); + }); + it('"ImportRecordModal" component should be closed when the user clicks "handleClose" button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'Overlay source bibliographic record' })); + userEvent.click(screen.getByRole('button', { name: 'handleCancel' })); + expect(screen.queryByText('ImportRecordModal')).not.toBeInTheDocument(); + }); + it('NewOrderModal should render when the user clicks the new order button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'New order' })); + expect(screen.queryByText(/Create order/i)).toBeInTheDocument(); + }); + it('push function should be called when the user clicks the "Edit MARC bibliographic record" button', async () => { + renderViewInstance(); + const expectedValue = { + pathname: `/inventory/quick-marc/edit-bib/${defaultProp.selectedInstance.id}`, + search: 'filters=test1&query=test2&sort=test3&qindex=test' + }; + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + const button = screen.getByRole('button', { name: 'Edit MARC bibliographic record' }); + await waitFor(() => { + expect(button).not.toHaveAttribute('disabled'); + }); + userEvent.click(button); + expect(mockPush).toBeCalledWith(expectedValue); + }); + it('push function should be called when the user clicks the "Derive new MARC bibliographic record" button', async () => { + renderViewInstance(); + const expectedValue = { + pathname: `/inventory/quick-marc/duplicate-bib/${defaultProp.selectedInstance.id}`, + search: 'filters=test1&query=test2&sort=test3&qindex=test' + }; + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + const button = screen.getByRole('button', { name: 'Derive new MARC bibliographic record' }); + await waitFor(() => { + expect(button).not.toHaveAttribute('disabled'); + }); + userEvent.click(button); + expect(mockPush).toBeCalledWith(expectedValue); + }); + it('NewOrderModal should be closed when the user clicks the close button', async () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'Actions' })); + userEvent.click(screen.getByRole('button', { name: 'New order' })); + expect(screen.queryByText(/Create order/i)).toBeInTheDocument(); + userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + await waitFor(() => { + expect(screen.queryByText(/Create order/i)).not.toBeInTheDocument(); + }); + }); + }); + describe('Tests for shortcut of HasCommand', () => { + it('updateLocation function to be triggered on clicking new button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'new' })); + expect(defaultProp.updateLocation).toBeCalled(); + }); + it('onClickEditInstance function to be triggered on clicking edit button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'edit' })); + expect(mockPush).toBeCalled(); + }); + it('onCopy function to be triggered on clicking duplicateRecord button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'duplicateRecord' })); + expect(defaultProp.onCopy).toBeCalled(); + }); + it('collapseAllSections triggered on clicking collapseAllSections button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'collapseAllSections' })); + expect(spyOncollapseAllSections).toBeCalled(); + }); + it('expandAllSections triggered on clicking expandAllSections button', () => { + renderViewInstance(); + userEvent.click(screen.getByRole('button', { name: 'expandAllSections' })); + expect(spyOnexpandAllSections).toBeCalled(); }); }); }); diff --git a/test/jest/__mock__/stripesComponents.mock.js b/test/jest/__mock__/stripesComponents.mock.js index 6af025fbf..b934b4beb 100644 --- a/test/jest/__mock__/stripesComponents.mock.js +++ b/test/jest/__mock__/stripesComponents.mock.js @@ -2,7 +2,8 @@ import React from 'react'; jest.mock('@folio/stripes/components', () => ({ ...jest.requireActual('@folio/stripes/components'), - ConfirmationModal: jest.fn(({ heading, message, onConfirm, onCancel }) => ( + collapseAllSections: jest.fn(), + ConfirmationModal: jest.fn(({ heading, message, onConfirm, onCancel, onRemove }) => (
ConfirmationModal {heading} @@ -10,9 +11,11 @@ jest.mock('@folio/stripes/components', () => ({
+
)), + expandAllSections: jest.fn(), formattedLanguageName: jest.fn((languageCode) => { switch (languageCode) { case 'en': @@ -25,8 +28,17 @@ jest.mock('@folio/stripes/components', () => ({ return ''; } }), + HasCommand: (props) => { + const { commands, children } = props; + const component = + <> + {commands.map((shortcut, index) => ( + + ))}{children}; + ; + return component; + }, Loading: () =>
Loading
, LoadingPane: () =>
LoadingPane
, LoadingView: () =>
LoadingView
, - LoadingPane: () =>
LoadingPane
, }), { virtual: true }); From 88d87f07bb5b5f5ba8e00f516281720d7d86cc4f Mon Sep 17 00:00:00 2001 From: ssandupatla <58558770+ssandupatla@users.noreply.github.com> Date: Wed, 10 May 2023 11:27:50 +0530 Subject: [PATCH 16/68] UIIN-1747 JEST/RTL test cases for utils.js (#2110) --- src/Instance/ViewRequests/utils.test.js | 163 ++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/Instance/ViewRequests/utils.test.js diff --git a/src/Instance/ViewRequests/utils.test.js b/src/Instance/ViewRequests/utils.test.js new file mode 100644 index 000000000..9c0897443 --- /dev/null +++ b/src/Instance/ViewRequests/utils.test.js @@ -0,0 +1,163 @@ +import { + getRecordsInBatch, + buildQueryByHoldingsIds, + buildQueryByItemsIds, + batchFetch, + batchFetchItems, + batchFetchRequests, +} from './utils'; + +import '../../../test/jest/__mock__'; + +describe('getRecordsInBatch', () => { + const mockMutator = { + GET: jest.fn(() => Promise.resolve()), + reset: jest.fn(), + }; + it('should return an empty array if no records are found', async () => { + mockMutator.GET.mockReturnValueOnce(Promise.resolve({ totalRecords: 0 })); + const result = await getRecordsInBatch(mockMutator, {}); + expect(result).toEqual([]); + }); + it('should return all records in one batch if there are fewer records than the limit', async () => { + const records = [{ id: '1' }, { id: '2' }, { id: '3' }]; + mockMutator.GET.mockReturnValueOnce(Promise.resolve({ totalRecords: records.length })); + mockMutator.GET.mockImplementationOnce(({ params }) => { + const offset = params.offset || 0; + const recordsChunk = records.slice(offset, offset + params.limit); + return Promise.resolve(recordsChunk); + }); + const result = await getRecordsInBatch(mockMutator, {}); + expect(result).toEqual(records); + }); + it('should return all records in multiple batches if there are more records than the limit', async () => { + const records = [{ id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' }, { id: '6' }, { id: '7' }, { id: '8' }]; + mockMutator.GET.mockReturnValueOnce(Promise.resolve({ totalRecords: records.length })); + mockMutator.GET.mockImplementation(({ params }) => { + const offset = params.offset || 0; + const recordsChunk = records.slice(offset, offset + params.limit); + return Promise.resolve(recordsChunk); + }); + const result = await getRecordsInBatch(mockMutator, {}); + expect(result).toEqual(records); + }); +}); + +describe('buildQueryByHoldingsIds', () => { + it('should return empty string when given an empty array', () => { + const query = buildQueryByHoldingsIds([]); + expect(query).toBe(''); + }); + it('should return a query string when given an array of holdings', () => { + const holdings = [ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ]; + const query = buildQueryByHoldingsIds(holdings); + expect(query).toBe('holdingsRecordId==1 or holdingsRecordId==2 or holdingsRecordId==3'); + }); + it('should handle a single holding in the array', () => { + const holdings = [ + { id: 1 }, + ]; + const query = buildQueryByHoldingsIds(holdings); + expect(query).toBe('holdingsRecordId==1'); + }); +}); + +describe('buildQueryByItemsIds', () => { + it('returns empty string for empty input', () => { + const result = buildQueryByItemsIds([]); + expect(result).toEqual(''); + }); + it('returns correct query for single item', () => { + const result = buildQueryByItemsIds([{ id: 123 }]); + expect(result).toEqual('itemId==123'); + }); + it('returns correct query for multiple items', () => { + const result = buildQueryByItemsIds([{ id: 123 }, { id: 456 }, { id: 789 }]); + expect(result).toEqual('itemId==123 or itemId==456 or itemId==789'); + }); + it('returns empty string for input without ids', () => { + const result = buildQueryByItemsIds([{ foo: 'bar' }]); + expect(result).toEqual('itemId==undefined'); + }); +}); + +describe('batchFetch', () => { + const mutator = { + reset: jest.fn(), + GET: jest.fn(), + }; + beforeEach(() => { + mutator.reset.mockClear(); + mutator.GET.mockClear(); + }); + it('should return empty array when no records are provided', async () => { + const records = []; + const buildQuery = jest.fn(); + const response = await batchFetch(mutator, records, buildQuery); + expect(response).toEqual([]); + expect(mutator.reset).toHaveBeenCalledTimes(1); + expect(mutator.GET).toHaveBeenCalledTimes(0); + }); + it('should return empty array when query is empty', async () => { + const records = [{ id: 1 }]; + const buildQuery = jest.fn(() => ''); + const response = await batchFetch(mutator, records, buildQuery); + expect(response).toEqual([]); + expect(mutator.reset).toHaveBeenCalledTimes(1); + expect(mutator.GET).toHaveBeenCalledTimes(0); + }); + it('should return records when mutator.GET returns data', async () => { + const records = [{ id: 1 }, { id: 2 }]; + const buildQuery = buildQueryByHoldingsIds; + const data = []; + mutator.GET.mockResolvedValueOnce({ data }); + const response = await batchFetch(mutator, records, buildQuery); + expect(response).toEqual(data); + expect(mutator.reset).toHaveBeenCalledTimes(1); + expect(mutator.GET).toHaveBeenCalledTimes(1); + expect(mutator.GET).toHaveBeenCalledWith({ + params: { + limit: 0, + query: '(holdingsRecordId==1 or holdingsRecordId==2)', + }, + records: undefined, + }); + }); +}); + +describe('batchFetchItems', () => { + const mockMutator = { + GET: jest.fn(() => Promise.resolve()), + reset: jest.fn(), + }; + it('returns an empty array if holdings are empty', async () => { + const result = await batchFetchItems(mockMutator, []); + expect(result).toEqual([]); + }); +}); + +describe('batchFetchRequests', () => { + let mutator; + beforeEach(() => { + mutator = { + GET: jest.fn(), + reset: jest.fn(), + }; + }); + it('should return empty array if items array is empty', async () => { + const result = await batchFetchRequests(mutator, []); + expect(result).toEqual([]); + }); + it('should flatten the response arrays and return the concatenated records', async () => { + const records = [{ id: 1 }, { id: 2 }]; + mutator.GET.mockReturnValueOnce(Promise.resolve({ totalRecords: 2 })); + mutator.GET.mockReturnValueOnce(Promise.resolve(records)); + const items = [{ id: 1 }, { id: 2 }]; + const result = await batchFetchRequests(mutator, items); + expect(result).toEqual(records); + }); +}); From 66e39a7ad729c07102433ba01e0d735f298f35ad Mon Sep 17 00:00:00 2001 From: cchitneedi <100114810+cchitneedi@users.noreply.github.com> Date: Wed, 10 May 2023 11:47:58 +0530 Subject: [PATCH 17/68] UIIN-1700 create Jest/RTL test for ViewHoldingsRecord.js (#2146) --- src/ViewHoldingsRecord.test.js | 104 ++++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/src/ViewHoldingsRecord.test.js b/src/ViewHoldingsRecord.test.js index b17c450ad..9b8594a8f 100644 --- a/src/ViewHoldingsRecord.test.js +++ b/src/ViewHoldingsRecord.test.js @@ -1,25 +1,27 @@ import '../test/jest/__mock__'; - +import React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { MemoryRouter } from 'react-router-dom'; -import user from '@testing-library/user-event'; +import userEvent from '@testing-library/user-event'; import { screen, waitFor } from '@testing-library/react'; import { renderWithIntl, translationsProperties } from '../test/jest/helpers'; import ViewHoldingsRecord from './ViewHoldingsRecord'; + jest.mock('./withLocation', () => jest.fn(c => c)); -jest.mock('@folio/stripes/components', () => ({ - ...jest.requireActual('@folio/stripes/components'), - LoadingView: () => 'LoadingView', -})); + +const spyOncollapseAllSections = jest.spyOn(require('@folio/stripes/components'), 'collapseAllSections'); +const spyOnexpandAllSections = jest.spyOn(require('@folio/stripes/components'), 'expandAllSections'); + +const mockData = jest.fn().mockResolvedValue({ id: 'testId' }); const defaultProps = { id: 'id', goTo: jest.fn(), holdingsrecordid: 'holdingId', referenceTables: { - holdingsSources: [{ id: 'sourceId' }], + holdingsSources: [{ id: 'sourceId', name: 'MARC' }], locationsById: { inactiveLocation: { name: 'Location 1', isActive: false }, }, @@ -37,7 +39,7 @@ const defaultProps = { }, mutator: { instances1: { - GET: jest.fn(() => Promise.resolve({ id: 'instanceId' })), + GET: jest.fn(() => Promise.resolve({ id: 'instanceId', source: 'testSource' })), reset: jest.fn(() => Promise.resolve()), }, holdingsRecords: { @@ -48,7 +50,7 @@ const defaultProps = { reset: jest.fn(() => Promise.resolve()), }, marcRecord: { - GET: jest.fn(() => Promise.resolve({ id: 'marcRecordId' })), + GET: mockData, DELETE: jest.fn(() => Promise.resolve()), }, marcRecordId: { @@ -63,6 +65,7 @@ const defaultProps = { }, location: { search: '/', + pathname: 'pathname' }, }; @@ -82,53 +85,98 @@ const renderViewHoldingsRecord = (props = {}) => renderWithIntl( describe('ViewHoldingsRecord actions', () => { beforeEach(() => { - defaultProps.history.push.mockClear(); + jest.clearAllMocks(); }); it('should render Loading when awaiting resource', () => { const { getByText } = renderViewHoldingsRecord({ referenceTables: {} }); - expect(getByText('LoadingView')).toBeDefined(); }); it('should close view holding page', async () => { renderViewHoldingsRecord(); - - await waitFor(() => { - const closeBtn = screen.getAllByRole('button')[0]; - - user.click(closeBtn); - - expect(defaultProps.history.push).toHaveBeenCalled(); - }); + userEvent.click(await screen.findByRole('button', { name: 'confirm' })); + expect(defaultProps.history.push).toBeCalled(); }); it('should translate to edit holding form page', async () => { renderViewHoldingsRecord(); - const editHoldingBtn = await screen.findByTestId('edit-holding-btn'); - - user.click(editHoldingBtn); - + userEvent.click(editHoldingBtn); expect(defaultProps.history.push).toHaveBeenCalled(); }); it('should translate to duplicate holding form page', async () => { renderViewHoldingsRecord(); - const duplicatHoldingBtn = await screen.findByTestId('duplicate-holding-btn'); - - user.click(duplicatHoldingBtn); - + userEvent.click(duplicatHoldingBtn); expect(defaultProps.history.push).toHaveBeenCalled(); }); it('should display "inactive" by an inactive temporary location', async () => { renderViewHoldingsRecord(); - await waitFor(() => { const tempLocation = document.querySelector('*[data-test-id=temporary-location]').innerHTML; expect(tempLocation).toContain('Inactive'); }); }); + it('handleViewSource should be called when View Source button', async () => { + renderViewHoldingsRecord(); + userEvent.click(await screen.findByText('Actions')); + const viewSourceBtn = screen.getByRole('button', { name: 'View source' }); + await waitFor(() => { + expect(viewSourceBtn).not.toHaveAttribute('disabled'); + }); + userEvent.click(viewSourceBtn); + expect(defaultProps.goTo).toBeCalled(); + }); + it('"handleEditInQuickMarc" function should be called when "Edit in quickMARC" button', async () => { + renderViewHoldingsRecord(); + userEvent.click(await screen.findByText('Actions')); + const quickMarcbtn = screen.getByRole('button', { name: 'Edit in quickMARC' }); + await waitFor(() => { + expect(quickMarcbtn).not.toHaveAttribute('disabled'); + }); + userEvent.click(quickMarcbtn); + expect(defaultProps.goTo).toBeCalled(); + }); + describe('Tests for shortcut of HasCommand', () => { + it('"onCopyHolding" function to be triggered on clicking "duplicateRecord" button', async () => { + const data = { + pathname: `/inventory/copy/${defaultProps.id}/${defaultProps.holdingsrecordid}`, + search: defaultProps.location.search, + state: { backPathname: defaultProps.location.pathname }, + }; + renderViewHoldingsRecord(); + userEvent.click(await screen.findByRole('button', { name: 'duplicateRecord' })); + expect(defaultProps.history.push).toBeCalledWith(data); + }); + it('"onEditHolding" function to be triggered on clicking edit button', async () => { + const data = { + pathname: `/inventory/edit/${defaultProps.id}/${defaultProps.holdingsrecordid}`, + search: defaultProps.location.search, + state: { backPathname: defaultProps.location.pathname }, + }; + renderViewHoldingsRecord(); + userEvent.click(await screen.findByRole('button', { name: 'edit' })); + expect(defaultProps.history.push).toBeCalledWith(data); + }); + it('"goTo" function to be triggered on clicking duplicateRecord button', async () => { + renderViewHoldingsRecord(); + userEvent.click(await screen.findByRole('button', { name: 'search' })); + expect(defaultProps.goTo).toBeCalledWith('/inventory'); + }); + it('collapseAllSections triggered on clicking collapseAllSections button', async () => { + renderViewHoldingsRecord(); + userEvent.click(await screen.findByRole('button', { name: 'collapseAllSections' })); + expect(spyOncollapseAllSections).toBeCalled(); + }); + it('expandAllSections triggered on clicking expandAllSections button', async () => { + renderViewHoldingsRecord(); + userEvent.click(await screen.findByRole('button', { name: 'expandAllSections' })); + await waitFor(() => { + expect(spyOnexpandAllSections).toBeCalled(); + }); + }); + }); }); From 54edcda7d6ec3c8644f4a30adaf7236011688dd0 Mon Sep 17 00:00:00 2001 From: Alisher Musurmonov Date: Thu, 11 May 2023 00:59:27 +0500 Subject: [PATCH 18/68] fix: display chronology column in receiving history (#2151) --- CHANGELOG.md | 1 + .../HoldingReceivingHistory/HoldingReceivingHistory.js | 5 ++++- .../HoldingReceivingHistory/HoldingReceivingHistory.test.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbdc63cf8..d932f035e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * The 'Missing' and 'Withdrawn' options in the actions menu are absent after clicking on the 'Actions' button for item status "In process". Fixes UIIN-2338. * Item Create/Edit screens: Replace custom RepeatableField with component from stripes. Refs UIIN-2397. * Item Create/Edit screens: Repeatable field trashcan is not aligned with the data row. Fixes UIIN-2374. +* Chronology not displayed in receiving history. Fixes UIIN-2411. ## [9.4.5](https://github.com/folio-org/ui-inventory/tree/v9.4.5) (2023-04-03) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.4...v9.4.5) diff --git a/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.js b/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.js index fde86634b..9e889a310 100644 --- a/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.js +++ b/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.js @@ -20,15 +20,17 @@ const columnMapping = { 'caption': , 'copyNumber': , 'enumeration': , + 'chronology': , 'receivedDate': , 'comment': , 'source': , }; -const visibleColumns = ['caption', 'copyNumber', 'enumeration', 'receivedDate', 'comment', 'source']; +const visibleColumns = ['caption', 'copyNumber', 'enumeration', 'chronology', 'receivedDate', 'comment', 'source']; const columnFormatter = { 'caption': i => i.caption || , 'copyNumber': i => i.copyNumber || , 'enumeration': i => i.enumeration || , + 'chronology': i => i.chronology || , 'receivedDate': i => (i.receivedDate ? : ), 'comment': i => i.comment || , 'source': i => , @@ -36,6 +38,7 @@ const columnFormatter = { const sorters = { 'caption': ({ caption }) => caption, 'copyNumber': ({ copyNumber }) => copyNumber, + 'chronology': ({ chronology }) => chronology, 'enumeration': ({ enumeration }) => enumeration, 'receivedDate': ({ receivedDate }) => receivedDate, 'source': ({ source }) => source, diff --git a/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.test.js b/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.test.js index bdbf0b755..f66e4aeff 100644 --- a/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.test.js +++ b/src/Holding/ViewHolding/HoldingReceivingHistory/HoldingReceivingHistory.test.js @@ -28,6 +28,7 @@ describe('HoldingReceivingHistory', () => { renderHoldingReceivingHistory({ id: 'holdingUid' }); expect(screen.getByText(receivingHistory[0].enumeration)).toBeInTheDocument(); + expect(screen.getByText(receivingHistory[0].chronology)).toBeInTheDocument(); }); it('should apply sort by a column', () => { From 91ae8f979ab716b3309a0cd0b7ed9f8d844ed317 Mon Sep 17 00:00:00 2001 From: Dmitriy-Litvinenko Date: Wed, 10 May 2023 13:29:47 +0300 Subject: [PATCH 19/68] UIIN-2412: Also support circulation 14.0 --- CHANGELOG.md | 1 + package.json | 2 +- test/bigtest/network/factories/request.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d932f035e..da97419c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Item Create/Edit screens: Replace custom RepeatableField with component from stripes. Refs UIIN-2397. * Item Create/Edit screens: Repeatable field trashcan is not aligned with the data row. Fixes UIIN-2374. * Chronology not displayed in receiving history. Fixes UIIN-2411. +* Also support `circulation` `14.0`. Refs UIIN-2412. ## [9.4.5](https://github.com/folio-org/ui-inventory/tree/v9.4.5) (2023-04-03) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.4...v9.4.5) diff --git a/package.json b/package.json index 6b5f32e53..e12e7075d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "alternative-title-types": "1.0", "call-number-types": "1.0", "browse": "0.6 1.0", - "circulation": "9.0 10.0 11.0 12.0 13.0", + "circulation": "9.0 10.0 11.0 12.0 13.0 14.0", "classification-types": "1.1", "configuration": "2.0", "contributor-name-types": "1.2", diff --git a/test/bigtest/network/factories/request.js b/test/bigtest/network/factories/request.js index d672d647e..e1d815e04 100644 --- a/test/bigtest/network/factories/request.js +++ b/test/bigtest/network/factories/request.js @@ -7,7 +7,7 @@ export default Factory.extend({ requestDate: () => faker.date.past().toISOString().substring(0, 10), status: () => 'Open - Not yet filled', position: (i) => i + 1, - fulfilmentPreference: 'Hold Shelf', + fulfillmentPreference: 'Hold Shelf', holdShelfExpirationDate: '2017-01-20', loan: { dueDate: '2017-09-19T12:42:21.000Z', From 9d27467261e4bb9c2ea218cd0bd84ff53cccbe37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Turek?= <128044179+przemyslawturek@users.noreply.github.com> Date: Tue, 16 May 2023 14:35:16 +0200 Subject: [PATCH 20/68] UIIN-2354 Validator Instance/Holdings/Item notes character limit 32K (#2149) * UIIN-2354 Validator Instance/Holdings/Item notes character limit 32K * UIIN-2354 Changed validator name, moved const to constants file * UIIN-2354 Added unit test for validate form in utils * UIIN-2354 Added mock to test * UIIN-2354 Change description for tests --- CHANGELOG.md | 1 + src/constants.js | 1 + src/edit/administrativeNoteFields.js | 10 ++ .../holdings/repeatableFields/NoteFields.js | 11 ++ src/edit/noteFields.js | 11 ++ src/utils.test.js | 109 ++++++++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 src/utils.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index da97419c7..90d039e6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Item Create/Edit screens: Repeatable field trashcan is not aligned with the data row. Fixes UIIN-2374. * Chronology not displayed in receiving history. Fixes UIIN-2411. * Also support `circulation` `14.0`. Refs UIIN-2412. +* Instance/Holdings/Item notes, administrative notes character limit to 32K. Refs UIIN-2354. ## [9.4.5](https://github.com/folio-org/ui-inventory/tree/v9.4.5) (2023-04-03) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.4...v9.4.5) diff --git a/src/constants.js b/src/constants.js index 720908d47..006396aeb 100644 --- a/src/constants.js +++ b/src/constants.js @@ -416,6 +416,7 @@ export const PAGE_DIRECTIONS = { }; export const BROWSE_RESULTS_COUNT = 100; +export const NOTE_CHARS_MAX_LENGTH = 32000; export const ORDERS_API = 'orders/composite-orders'; diff --git a/src/edit/administrativeNoteFields.js b/src/edit/administrativeNoteFields.js index d46eb3c12..2fff1d969 100644 --- a/src/edit/administrativeNoteFields.js +++ b/src/edit/administrativeNoteFields.js @@ -11,10 +11,19 @@ import { TextArea, } from '@folio/stripes/components'; +import { + validateFieldLength +} from '../utils'; + +import { + NOTE_CHARS_MAX_LENGTH +} from '../constants'; + const AdministrativeNoteFields = () => { const { formatMessage } = useIntl(); const administrativeNoteLabel = formatMessage({ id: 'ui-inventory.administrativeNote' }); + const validate = value => validateFieldLength(value, NOTE_CHARS_MAX_LENGTH); const legend = (