diff --git a/CHANGELOG.md b/CHANGELOG.md index 51e5461f8..9415fd215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ * *BREAKING* Instance: Display all versions in View history fourth pane. Refs UIIN-3173. * *BREAKING* MARC bib > View Source > Display Version History. Refs UIIN-3261. * "Copy barcode" icon is displayed next to item with no barcode. Fixes UIIN-3141. +* *BREAKING* Holdings: Display all versions in View history second pane. Refs UIIN-3174. ## [12.0.12](https://github.com/folio-org/ui-inventory/tree/v12.0.12) (2025-01-27) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v12.0.11...v12.0.12) diff --git a/package.json b/package.json index ed2882e8f..2ec3896c5 100644 --- a/package.json +++ b/package.json @@ -685,7 +685,8 @@ "mod-settings.global.read.ui-inventory.display-settings.manage", "inventory-storage.instance-date-types.collection.get", "audit.inventory.instance.collection.get", - "audit.marc.bib.collection.get" + "audit.marc.bib.collection.get", + "audit.inventory.holdings.collection.get" ], "visible": true }, diff --git a/src/Holding/HoldingVersionHistory/HoldingVersionHistory.js b/src/Holding/HoldingVersionHistory/HoldingVersionHistory.js new file mode 100644 index 000000000..c2694e29f --- /dev/null +++ b/src/Holding/HoldingVersionHistory/HoldingVersionHistory.js @@ -0,0 +1,111 @@ +import { + useContext, + useState, +} from 'react'; +import { useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; + +import { AuditLogPane } from '@folio/stripes/components'; + +import { + useHoldingAuditDataQuery, + useInventoryVersionHistory, +} from '../../hooks'; +import { DataContext } from '../../contexts'; + +export const getFieldFormatter = referenceData => ({ + discoverySuppress: value => value.toString(), + holdingsTypeId: value => referenceData.holdingsTypes?.find(type => type.id === value)?.name, + statisticalCodeIds: value => { + const statisticalCode = referenceData.statisticalCodes?.find(code => code.id === value); + + return `${statisticalCode.statisticalCodeType.name}: ${statisticalCode.code} - ${statisticalCode.name}`; + }, + callNumberTypeId: value => referenceData.callNumberTypes?.find(type => type.id === value)?.name, + permanentLocationId: value => referenceData.locationsById[value]?.name, + temporaryLocationId: value => referenceData.locationsById[value]?.name, + effectiveLocationId: value => referenceData.locationsById[value]?.name, + illPolicyId: value => referenceData.illPolicies.find(policy => policy.id === value)?.name, + staffOnly: value => value.toString(), + holdingsNoteTypeId: value => referenceData.holdingsNoteTypes?.find(noteType => noteType.id === value)?.name, + relationshipId: value => referenceData.electronicAccessRelationships?.find(noteType => noteType.id === value)?.name, + publicDisplay: value => value.toString(), +}); + +const HoldingVersionHistory = ({ onClose, holdingId }) => { + const { formatMessage } = useIntl(); + const referenceData = useContext(DataContext); + + const [lastVersionEventTs, setLastVersionEventTs] = useState(null); + + const { + data, + totalRecords, + isLoading, + } = useHoldingAuditDataQuery(holdingId, lastVersionEventTs); + + const { + actionsMap, + versions, + isLoadMoreVisible, + } = useInventoryVersionHistory({ data, totalRecords }); + + const fieldLabelsMap = { + discoverySuppress: formatMessage({ id: 'ui-inventory.discoverySuppressed' }), + hrid: formatMessage({ id: 'ui-inventory.holdingsHrid' }), + sourceId: formatMessage({ id: 'ui-inventory.holdingsSourceLabel' }), + formerIds: formatMessage({ id: 'ui-inventory.formerHoldingsId' }), + holdingsTypeId: formatMessage({ id: 'ui-inventory.holdingsType' }), + statisticalCodeIds: formatMessage({ id: 'ui-inventory.statisticalCodes' }), + administrativeNotes: formatMessage({ id: 'ui-inventory.administrativeNotes' }), + permanentLocationId: formatMessage({ id: 'ui-inventory.permanentLocation' }), + temporaryLocationId: formatMessage({ id: 'ui-inventory.temporaryLocation' }), + effectiveLocationId: formatMessage({ id: 'ui-inventory.effectiveLocationHoldings' }), + shelvingOrder: formatMessage({ id: 'ui-inventory.shelvingOrder' }), + shelvingTitle: formatMessage({ id: 'ui-inventory.shelvingTitle' }), + copyNumber: formatMessage({ id: 'ui-inventory.copyNumber' }), + callNumberTypeId: formatMessage({ id: 'ui-inventory.callNumberType' }), + callNumberPrefix: formatMessage({ id: 'ui-inventory.callNumberPrefix' }), + callNumber: formatMessage({ id: 'ui-inventory.callNumber' }), + callNumberSuffix: formatMessage({ id: 'ui-inventory.callNumberSuffix' }), + numberOfItems: formatMessage({ id: 'ui-inventory.numberOfItems' }), + holdingsStatements: formatMessage({ id: 'ui-inventory.holdingsStatements' }), + holdingsStatementsForIndexes: formatMessage({ id: 'ui-inventory.holdingsStatementForIndexes' }), + holdingsStatementsForSupplements: formatMessage({ id: 'ui-inventory.holdingsStatementForSupplements' }), + digitizationPolicy: formatMessage({ id: 'ui-inventory.digitizationPolicy' }), + illPolicyId: formatMessage({ id: 'ui-inventory.illPolicy' }), + retentionPolicy: formatMessage({ id: 'ui-inventory.retentionPolicy' }), + notes: formatMessage({ id: 'ui-inventory.notes' }), + electronicAccess: formatMessage({ id: 'ui-inventory.electronicAccess' }), + acquisitionFormat: formatMessage({ id: 'ui-inventory.acquisitionFormat' }), + acquisitionMethod: formatMessage({ id: 'ui-inventory.acquisitionMethod' }), + receiptStatus: formatMessage({ id: 'ui-inventory.receiptStatus' }), + entries: formatMessage({ id: 'ui-inventory.receivingHistory' }), + }; + + const fieldFormatter = getFieldFormatter(referenceData); + + const handleLoadMore = lastEventTs => { + setLastVersionEventTs(lastEventTs); + }; + + return ( + + ); +}; + +HoldingVersionHistory.propTypes = { + holdingId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default HoldingVersionHistory; diff --git a/src/Holding/HoldingVersionHistory/HoldingVersionHistory.test.js b/src/Holding/HoldingVersionHistory/HoldingVersionHistory.test.js new file mode 100644 index 000000000..63e1a7a89 --- /dev/null +++ b/src/Holding/HoldingVersionHistory/HoldingVersionHistory.test.js @@ -0,0 +1,117 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; +import { screen } from '@folio/jest-config-stripes/testing-library/react'; +import { runAxeTest } from '@folio/stripes-testing'; + +import { act } from 'react'; +import { + renderWithIntl, + translationsProperties, +} from '../../../test/jest/helpers'; + +import { DataContext } from '../../contexts'; +import HoldingVersionHistory, { getFieldFormatter } from './HoldingVersionHistory'; + +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), + AuditLogPane: () =>
Version history
, +})); + +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + useHoldingAuditDataQuery: jest.fn().mockReturnValue({ data: [{}], isLoading: false }), +})); + +const queryClient = new QueryClient(); + +const onCloseMock = jest.fn(); +const holdingId = 'holdingId'; +const mockReferenceData = { + holdingsTypes: [{ id: 'holding-type-1', name: 'Holding type 1' }], + statisticalCodes: [{ id: 'stat-1', statisticalCodeType: { name: 'Category' }, code: '001', name: 'Stat Code' }], + callNumberTypes: [{ id: '123', name: 'Test Call Number Type' }], + locationsById: { 'location-1': { name: 'Main Library' } }, + illPolicies: [{ id: 'ill-policy-1', name: 'Ill policy 1' }], + holdingsNoteTypes: [{ id: 'h-note-type-1', name: 'H note type 1' }], + electronicAccessRelationships: [{ id: 'rel-1', name: 'Online Access' }], +}; + +const renderHoldingVersionHistory = () => { + const component = ( + + + + + + ); + + return renderWithIntl(component, translationsProperties); +}; + +describe('HoldingVersionHistory', () => { + it('should be rendered with no axe errors', async () => { + const { container } = await act(async () => renderHoldingVersionHistory()); + + await runAxeTest({ rootNode: container }); + }); + + it('should render View history pane', () => { + renderHoldingVersionHistory(); + + expect(screen.getByText('Version history')).toBeInTheDocument(); + }); +}); + +describe('field formatter', () => { + const fieldFormatter = getFieldFormatter(mockReferenceData); + + it('should format discoverySuppress field correctly', () => { + expect(fieldFormatter.discoverySuppress(true)).toBe('true'); + expect(fieldFormatter.discoverySuppress(false)).toBe('false'); + }); + + it('should format holdingsTypeId field correctly', () => { + expect(fieldFormatter.holdingsTypeId('holding-type-1')).toBe('Holding type 1'); + }); + + it('should format statistical codes field correctly', () => { + expect(fieldFormatter.statisticalCodeIds('stat-1')).toBe('Category: 001 - Stat Code'); + }); + + it('should format typeId callNumberTypeId correctly', () => { + expect(fieldFormatter.callNumberTypeId('123')).toBe('Test Call Number Type'); + }); + + it('should format location IDs field correctly', () => { + expect(fieldFormatter.permanentLocationId('location-1')).toBe('Main Library'); + expect(fieldFormatter.effectiveLocationId('location-1')).toBe('Main Library'); + expect(fieldFormatter.temporaryLocationId('location-1')).toBe('Main Library'); + }); + + it('should format illPolicyId field correctly', () => { + expect(fieldFormatter.illPolicyId('ill-policy-1')).toBe('Ill policy 1'); + }); + + it('should format staffOnly field correctly', () => { + expect(fieldFormatter.staffOnly(true)).toBe('true'); + expect(fieldFormatter.staffOnly(false)).toBe('false'); + }); + + it('should format holdingsNoteTypeId field correctly', () => { + expect(fieldFormatter.holdingsNoteTypeId('h-note-type-1')).toBe('H note type 1'); + }); + + it('should format electronic access relationships field correctly', () => { + expect(fieldFormatter.relationshipId('rel-1')).toBe('Online Access'); + }); + + it('should format publicDisplay field correctly', () => { + expect(fieldFormatter.publicDisplay(true)).toBe('true'); + expect(fieldFormatter.publicDisplay(false)).toBe('false'); + }); +}); diff --git a/src/Holding/HoldingVersionHistory/index.js b/src/Holding/HoldingVersionHistory/index.js new file mode 100644 index 000000000..d08910caf --- /dev/null +++ b/src/Holding/HoldingVersionHistory/index.js @@ -0,0 +1 @@ +export { default as HoldingVersionHistory } from './HoldingVersionHistory'; diff --git a/src/ViewHoldingsRecord.js b/src/ViewHoldingsRecord.js index 327e4954b..5fa12b90c 100644 --- a/src/ViewHoldingsRecord.js +++ b/src/ViewHoldingsRecord.js @@ -72,7 +72,6 @@ import { emptyList, noValue, holdingsStatementTypes, - SOURCE_VALUES, } from './constants'; import { WarningMessage, @@ -82,7 +81,7 @@ import { import HoldingAcquisitions from './Holding/ViewHolding/HoldingAcquisitions'; import HoldingReceivingHistory from './Holding/ViewHolding/HoldingReceivingHistory'; import HoldingBoundWith from './Holding/ViewHolding/HoldingBoundWith'; -import { VersionHistory } from './views/VersionHistory'; +import { HoldingVersionHistory } from './Holding/HoldingVersionHistory'; import css from './View.css'; @@ -976,7 +975,7 @@ class ViewHoldingsRecord extends React.Component { actionMenu={(params) => !isVersionHistoryOpen && this.getPaneHeaderActionMenu(params)} lastMenu={( - {holdingsSourceName === SOURCE_VALUES.FOLIO && isVersionHistoryEnabled && ( + {isVersionHistoryEnabled && ( {this.state.isVersionHistoryOpen && ( - this.setState({ isVersionHistoryOpen: false })} /> )} diff --git a/src/ViewHoldingsRecord.test.js b/src/ViewHoldingsRecord.test.js index 0ebfe6f51..fb169929f 100644 --- a/src/ViewHoldingsRecord.test.js +++ b/src/ViewHoldingsRecord.test.js @@ -41,6 +41,11 @@ jest.mock('@folio/stripes-acq-components', () => ({ FindLocation: jest.fn(() => FindLocation), })); +jest.mock('@folio/stripes-components', () => ({ + ...jest.requireActual('@folio/stripes-components'), + useVersionHistory: () => ({ versions: [], isLoadMoreVisible: true }), +})); + jest.mock('./withLocation', () => jest.fn(c => c)); jest.mock('./common', () => ({ diff --git a/src/hooks/index.js b/src/hooks/index.js index ba50c439b..c5c4726bb 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -6,6 +6,7 @@ export { default as useHoldingItemsQuery } from './useHoldingItemsQuery'; export { default as useHoldingMutation } from './useHoldingMutation'; export { default as useHoldingsFromStorage } from './useHoldingsFromStorage'; export { default as useInstanceAuditDataQuery } from './useInstanceAuditDataQuery'; +export { default as useHoldingAuditDataQuery } from './useHoldingAuditDataQuery'; export { default as useInstanceMutation } from './useInstanceMutation'; export { default as useHoldingsQueryByHrids } from './useHoldingsQueryByHrids'; export { default as useInventoryBrowse } from './useInventoryBrowse'; diff --git a/src/hooks/useHoldingAuditDataQuery/index.js b/src/hooks/useHoldingAuditDataQuery/index.js new file mode 100644 index 000000000..774b898af --- /dev/null +++ b/src/hooks/useHoldingAuditDataQuery/index.js @@ -0,0 +1 @@ +export { default } from './useHoldingAuditDataQuery'; diff --git a/src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.js b/src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.js new file mode 100644 index 000000000..61d3308d5 --- /dev/null +++ b/src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.js @@ -0,0 +1,30 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; + +const useHoldingAuditDataQuery = (holdingId, eventTs) => { + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'holding-audit-data' }); + + // eventTs param is used to load more data + const { isLoading, data = {} } = useQuery({ + queryKey: [namespace, holdingId, eventTs], + queryFn: () => ky.get(`audit-data/inventory/holdings/${holdingId}`, { + searchParams: { + ...(eventTs && { eventTs }) + } + }).json(), + enabled: Boolean(holdingId), + }); + + return { + data: data?.inventoryAuditItems || [], + totalRecords: data?.totalRecords, + isLoading, + }; +}; + +export default useHoldingAuditDataQuery; diff --git a/src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.test.js b/src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.test.js new file mode 100644 index 000000000..005350594 --- /dev/null +++ b/src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.test.js @@ -0,0 +1,46 @@ +import React, { act } from 'react'; +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; + +import { renderHook } from '@folio/jest-config-stripes/testing-library/react'; +import { useOkapiKy } from '@folio/stripes/core'; + +import '../../../test/jest/__mock__'; + +import useHoldingAuditDataQuery from './useHoldingAuditDataQuery'; + +jest.mock('@folio/stripes/core', () => ({ + ...jest.requireActual('@folio/stripes/core'), + useOkapiKy: jest.fn(), +})); + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useHoldingAuditDataQuery', () => { + beforeEach(() => { + useOkapiKy.mockClear().mockReturnValue({ + get: () => ({ + json: () => Promise.resolve({ inventoryAuditItems: [{ action: 'UPDATE' }] }), + }), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch holdings', async () => { + const { result } = renderHook(() => useHoldingAuditDataQuery('holdingId'), { wrapper }); + + await act(() => !result.current.isLoading); + + expect(result.current.data).toEqual([{ action: 'UPDATE' }]); + }); +});