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' }]);
+ });
+});