Skip to content

Commit

Permalink
UIIN-3174: Holdings: Display all versions in View history second pane (
Browse files Browse the repository at this point in the history
  • Loading branch information
OleksandrHladchenko1 authored Mar 5, 2025
1 parent 2dba643 commit f19ea5c
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
111 changes: 111 additions & 0 deletions src/Holding/HoldingVersionHistory/HoldingVersionHistory.js
Original file line number Diff line number Diff line change
@@ -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 (
<AuditLogPane
versions={versions}
onClose={onClose}
isLoadMoreVisible={isLoadMoreVisible}
handleLoadMore={handleLoadMore}
isLoading={isLoading}
fieldLabelsMap={fieldLabelsMap}
fieldFormatter={fieldFormatter}
actionsMap={actionsMap}
/>
);
};

HoldingVersionHistory.propTypes = {
holdingId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};

export default HoldingVersionHistory;
117 changes: 117 additions & 0 deletions src/Holding/HoldingVersionHistory/HoldingVersionHistory.test.js
Original file line number Diff line number Diff line change
@@ -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: () => <div>Version history</div>,
}));

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 = (
<QueryClientProvider client={queryClient}>
<DataContext.Provider value={mockReferenceData}>
<HoldingVersionHistory
holdingId={holdingId}
onClose={onCloseMock}
/>
</DataContext.Provider>
</QueryClientProvider>
);

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');
});
});
1 change: 1 addition & 0 deletions src/Holding/HoldingVersionHistory/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as HoldingVersionHistory } from './HoldingVersionHistory';
8 changes: 4 additions & 4 deletions src/ViewHoldingsRecord.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ import {
emptyList,
noValue,
holdingsStatementTypes,
SOURCE_VALUES,
} from './constants';
import {
WarningMessage,
Expand All @@ -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';

Expand Down Expand Up @@ -976,7 +975,7 @@ class ViewHoldingsRecord extends React.Component {
actionMenu={(params) => !isVersionHistoryOpen && this.getPaneHeaderActionMenu(params)}
lastMenu={(
<PaneMenu>
{holdingsSourceName === SOURCE_VALUES.FOLIO && isVersionHistoryEnabled && (
{isVersionHistoryEnabled && (
<VersionHistoryButton
disabled={isVersionHistoryOpen}
onClick={this.openVersionHistory}
Expand Down Expand Up @@ -1341,7 +1340,8 @@ class ViewHoldingsRecord extends React.Component {
</AccordionStatus>
</Pane>
{this.state.isVersionHistoryOpen && (
<VersionHistory
<HoldingVersionHistory
holdingId={holdingsRecord.id}
onClose={() => this.setState({ isVersionHistoryOpen: false })}
/>
)}
Expand Down
5 changes: 5 additions & 0 deletions src/ViewHoldingsRecord.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ jest.mock('@folio/stripes-acq-components', () => ({
FindLocation: jest.fn(() => <span>FindLocation</span>),
}));

jest.mock('@folio/stripes-components', () => ({
...jest.requireActual('@folio/stripes-components'),
useVersionHistory: () => ({ versions: [], isLoadMoreVisible: true }),
}));

jest.mock('./withLocation', () => jest.fn(c => c));

jest.mock('./common', () => ({
Expand Down
1 change: 1 addition & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useHoldingAuditDataQuery/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useHoldingAuditDataQuery';
30 changes: 30 additions & 0 deletions src/hooks/useHoldingAuditDataQuery/useHoldingAuditDataQuery.js
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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 }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

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

0 comments on commit f19ea5c

Please sign in to comment.