Skip to content

Commit

Permalink
UIIN-3175: Item: Display all versions in View history second pane (#2763
Browse files Browse the repository at this point in the history
)
  • Loading branch information
OleksandrHladchenko1 authored Mar 5, 2025
1 parent f19ea5c commit 5093383
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
* *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.
* *BREAKING* Item: Display all versions in View history second pane. Refs UIIN-3175.

## [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
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,8 +685,9 @@
"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.inventory.holdings.collection.get"
"audit.inventory.holdings.collection.get",
"audit.inventory.item.collection.get",
"audit.marc.bib.collection.get"
],
"visible": true
},
Expand Down
133 changes: 133 additions & 0 deletions src/Item/ItemVersionHistory/ItemVersionHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useContext, useState } from 'react';
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';

import { AuditLogPane } from '@folio/stripes/components';

import { DataContext } from '../../contexts';
import {
useItemAuditDataQuery,
useInventoryVersionHistory,
} from '../../hooks';

import { getDateWithTime } from '../../utils';

export const createFieldFormatter = (referenceData, circulationHistory) => ({
discoverySuppress: value => value.toString(),
typeId: value => referenceData.callNumberTypes?.find(type => type.id === value)?.name,
itemLevelCallNumberTypeId: value => referenceData.callNumberTypes?.find(type => type.id === value)?.name,
itemDamagedStatusId: value => referenceData.itemDamagedStatuses?.find(type => type.id === value)?.name,
permanentLocationId: value => referenceData.locationsById[value]?.name,
temporaryLocationId: value => referenceData.locationsById[value]?.name,
effectiveLocationId: value => referenceData.locationsById[value]?.name,
permanentLoanTypeId: value => referenceData.loanTypes?.find(type => type.id === value)?.name,
temporaryLoanTypeId: value => referenceData.loanTypes?.find(type => type.id === value)?.name,
materialTypeId: value => referenceData.materialTypes?.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}`;
},
relationshipId: value => referenceData.electronicAccessRelationships?.find(type => type.id === value)?.name,
staffOnly: value => value.toString(),
itemNoteTypeId: value => referenceData.itemNoteTypes?.find(type => type.id === value)?.name,
date: value => getDateWithTime(value),
servicePointId: () => circulationHistory.servicePointName,
staffMemberId: () => circulationHistory.source,
dateTime: value => getDateWithTime(value),
source: value => `${value.personal.lastName}, ${value.personal.firstName}`,
});

const ItemVersionHistory = ({
onClose,
itemId,
circulationHistory,
}) => {
const { formatMessage } = useIntl();
const referenceData = useContext(DataContext);

const [lastVersionEventTs, setLastVersionEventTs] = useState(null);

const {
data,
totalRecords,
isLoading,
} = useItemAuditDataQuery(itemId, lastVersionEventTs);
const {
actionsMap,
versions,
isLoadMoreVisible,
} = useInventoryVersionHistory({ data, totalRecords });

const fieldLabelsMap = {
accessionNumber: formatMessage({ id: 'ui-inventory.accessionNumber' }),
administrativeNotes: formatMessage({ id: 'ui-inventory.administrativeNotes' }),
barcode: formatMessage({ id: 'ui-inventory.itemBarcode' }),
callNumber: formatMessage({ id: 'ui-inventory.effectiveCallNumber' }),
circulationNotes: formatMessage({ id: 'ui-inventory.circulationHistory' }),
chronology: formatMessage({ id: 'ui-inventory.chronology' }),
copyNumber: formatMessage({ id: 'ui-inventory.copyNumber' }),
date: formatMessage({ id: 'ui-inventory.date' }),
dateTime: formatMessage({ id: 'ui-inventory.checkInDate' }),
descriptionOfPieces: formatMessage({ id: 'ui-inventory.descriptionOfPieces' }),
discoverySuppress: formatMessage({ id: 'ui-inventory.discoverySuppress' }),
displaySummary: formatMessage({ id: 'ui-inventory.displaySummary' }),
effectiveLocationId: formatMessage({ id: 'ui-inventory.effectiveLocation' }),
electronicAccess: formatMessage({ id: 'ui-inventory.electronicAccess' }),
enumeration: formatMessage({ id: 'ui-inventory.enumeration' }),
formerIds: formatMessage({ id: 'ui-inventory.formerId' }),
itemDamagedStatusDate: formatMessage({ id: 'ui-inventory.itemDamagedStatusDate' }),
itemDamagedStatusId: formatMessage({ id: 'ui-inventory.itemDamagedStatus' }),
itemIdentifier: formatMessage({ id: 'ui-inventory.itemIdentifier' }),
itemLevelCallNumber: formatMessage({ id: 'ui-inventory.callNumber' }),
itemLevelCallNumberPrefix: formatMessage({ id: 'ui-inventory.callNumberPrefix' }),
itemLevelCallNumberSuffix: formatMessage({ id: 'ui-inventory.callNumberSuffix' }),
itemLevelCallNumberTypeId: formatMessage({ id: 'ui-inventory.callNumberType' }),
materialTypeId: formatMessage({ id: 'ui-inventory.materialType' }),
missingPieces: formatMessage({ id: 'ui-inventory.missingPieces' }),
missingPiecesDate: formatMessage({ id: 'ui-inventory.date' }),
name: formatMessage({ id: 'ui-inventory.item.availability.itemStatus' }),
notes: formatMessage({ id: 'ui-inventory.itemNotes' }),
numberOfMissingPieces: formatMessage({ id: 'ui-inventory.numberOfMissingPieces' }),
numberOfPieces: formatMessage({ id: 'ui-inventory.numberOfPieces' }),
permanentLoanTypeId: formatMessage({ id: 'ui-inventory.permanentLoantype' }),
permanentLocationId: formatMessage({ id: 'ui-inventory.permanentLocation' }),
prefix: formatMessage({ id: 'ui-inventory.callNumberPrefix' }),
servicePointId: formatMessage({ id: 'ui-inventory.servicePoint' }),
staffMemberId: formatMessage({ id: 'ui-inventory.source' }),
statisticalCodeIds: formatMessage({ id: 'ui-inventory.statisticalCodes' }),
suffix: formatMessage({ id: 'ui-inventory.callNumberSuffix' }),
temporaryLoanTypeId: formatMessage({ id: 'ui-inventory.temporaryLoantype' }),
temporaryLocationId: formatMessage({ id: 'ui-inventory.temporaryLocation' }),
typeId: formatMessage({ id: 'ui-inventory.callNumberType' }),
volume: formatMessage({ id: 'ui-inventory.volume' }),
yearCaption: formatMessage({ id: 'ui-inventory.yearCaption' }),
};

const fieldFormatter = createFieldFormatter(referenceData, circulationHistory);

const handleLoadMore = lastEventTs => {
setLastVersionEventTs(lastEventTs);
};

return (
<AuditLogPane
versions={versions}
onClose={onClose}
isLoadedMoreVisible={isLoadMoreVisible}
handleLoadMore={handleLoadMore}
isLoading={isLoading}
fieldLabelsMap={fieldLabelsMap}
fieldFormatter={fieldFormatter}
actionsMap={actionsMap}
/>
);
};

ItemVersionHistory.propTypes = {
itemId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
circulationHistory: PropTypes.object.isRequired,
};

export default ItemVersionHistory;
153 changes: 153 additions & 0 deletions src/Item/ItemVersionHistory/ItemVersionHistory.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { act } from 'react';
import {
QueryClient,
QueryClientProvider,
} from 'react-query';
import { screen } from '@folio/jest-config-stripes/testing-library/react';
import { runAxeTest } from '@folio/stripes-testing';

import {
renderWithIntl,
translationsProperties,
} from '../../../test/jest/helpers';

import ItemVersionHistory, { createFieldFormatter } from './ItemVersionHistory';
import { DataContext } from '../../contexts';

jest.mock('@folio/stripes/components', () => ({
...jest.requireActual('@folio/stripes/components'),
AuditLogPane: () => <div>Version history</div>,
}));

jest.mock('../../utils', () => ({
getDateWithTime: jest.fn(date => `Formatted Date: ${date}`),
}));

jest.mock('../../hooks', () => ({
...jest.requireActual('../../hooks'),
useItemAuditDataQuery: jest.fn().mockReturnValue({ data: [{}], isLoading: false }),
useInventoryVersionHistory: () => ({ versions: [], isLoadMoreVisible: true }),
}));

const queryClient = new QueryClient();

const onCloseMock = jest.fn();
const itemId = 'itemId';
const date = '2024-02-26T12:00:00Z';
const mockReferenceData = {
callNumberTypes: [{ id: '123', name: 'Test Call Number Type' }],
itemDamagedStatuses: [{ id: 'damaged-1', name: 'Damaged' }],
locationsById: { 'location-1': { name: 'Main Library' } },
loanTypes: [{ id: 'loan-1', name: 'Short Term' }],
materialTypes: [{ id: 'material-1', name: 'Book' }],
statisticalCodes: [{ id: 'stat-1', statisticalCodeType: { name: 'Category' }, code: '001', name: 'Stat Code' }],
electronicAccessRelationships: [{ id: 'rel-1', name: 'Online Access' }],
itemNoteTypes: [{ id: 'note-1', name: 'Public Note' }],
};
const mockCirculationHistory = {
servicePointName: 'Main Desk',
source: 'Librarian User',
};

const renderItemVersionHistory = () => {
const component = (
<QueryClientProvider client={queryClient}>
<DataContext.Provider value={{}}>
<ItemVersionHistory
itemId={itemId}
onClose={onCloseMock}
circulationHistory={{}}
/>
</DataContext.Provider>
</QueryClientProvider>
);

return renderWithIntl(component, translationsProperties);
};

describe('ItemVersionHistory', () => {
it('should be rendered with no axe errors', async () => {
const { container } = await act(async () => renderItemVersionHistory());

await runAxeTest({ rootNode: container });
});

it('should render View history pane', () => {
renderItemVersionHistory();

expect(screen.getByText('Version history')).toBeInTheDocument();
});
});

describe('createFieldFormatter', () => {
const fieldFormatter = createFieldFormatter(mockReferenceData, mockCirculationHistory);

it('should format discoverySuppress field correctly', () => {
expect(fieldFormatter.discoverySuppress(true)).toBe('true');
expect(fieldFormatter.discoverySuppress(false)).toBe('false');
});

it('should format typeId field correctly', () => {
expect(fieldFormatter.typeId('123')).toBe('Test Call Number Type');
});

it('should format typeId itemLevelCallNumberTypeId correctly', () => {
expect(fieldFormatter.itemLevelCallNumberTypeId('123')).toBe('Test Call Number Type');
});

it('should format itemDamagedStatusId field correctly', () => {
expect(fieldFormatter.itemDamagedStatusId('damaged-1')).toBe('Damaged');
});

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 loan types field correctly', () => {
expect(fieldFormatter.permanentLoanTypeId('loan-1')).toBe('Short Term');
expect(fieldFormatter.temporaryLoanTypeId('loan-1')).toBe('Short Term');
});

it('should format material types field correctly', () => {
expect(fieldFormatter.materialTypeId('material-1')).toBe('Book');
});

it('should format statistical codes field correctly', () => {
expect(fieldFormatter.statisticalCodeIds('stat-1')).toBe('Category: 001 - Stat Code');
});

it('should format electronic access relationships field correctly', () => {
expect(fieldFormatter.relationshipId('rel-1')).toBe('Online Access');
});

it('should format item note types field correctly', () => {
expect(fieldFormatter.itemNoteTypeId('note-1')).toBe('Public Note');
});

it('should format staffOnly field correctly', () => {
expect(fieldFormatter.staffOnly(true)).toBe('true');
expect(fieldFormatter.staffOnly(false)).toBe('false');
});

it('should format date field correctly', () => {
expect(fieldFormatter.date(date)).toBe(`Formatted Date: ${date}`);
});

it('should format dateTime field correctly', () => {
expect(fieldFormatter.dateTime(date)).toBe(`Formatted Date: ${date}`);
});

it('should format servicePointId field correctly', () => {
expect(fieldFormatter.servicePointId()).toBe('Main Desk');
});

it('should format staffMemberId field correctly', () => {
expect(fieldFormatter.staffMemberId()).toBe('Librarian User');
});

it('should format source field correctly', () => {
expect(fieldFormatter.source({ personal: { firstName: 'John', lastName: 'Doe' } })).toBe('Doe, John');
});
});
1 change: 1 addition & 0 deletions src/Item/ItemVersionHistory/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ItemVersionHistory } from './ItemVersionHistory';
1 change: 1 addition & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { default as useClassificationIdentifierTypes } from './useClassification
export { default as useClassificationBrowseConfig } from './useClassificationBrowseConfig';
export { default as useUpdateOwnership } from './useUpdateOwnership';
export { default as useLocalStorageItems } from './useLocalStorageItems';
export { default as useItemAuditDataQuery } from './useItemAuditDataQuery';
export { default as useInventoryVersionHistory } from './useInventoryVersionHistory';
export * from './useQuickExport';
export * from '@folio/stripes-inventory-components/lib/queries/useInstanceDateTypes';
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useItemAuditDataQuery/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './useItemAuditDataQuery';
30 changes: 30 additions & 0 deletions src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.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 useItemAuditDataQuery = (itemId, eventTs) => {
const ky = useOkapiKy();
const [namespace] = useNamespace({ key: 'item-audit-data' });

// eventTs param is used to load more data
const { isLoading, data = {} } = useQuery({
queryKey: [namespace, itemId, eventTs],
queryFn: () => ky.get(`audit-data/inventory/item/${itemId}`, {
searchParams: {
...(eventTs && { eventTs })
}
}).json(),
enabled: Boolean(itemId),
});

return {
data: data?.inventoryAuditItems || [],
totalRecords: data?.totalRecords,
isLoading,
};
};

export default useItemAuditDataQuery;
46 changes: 46 additions & 0 deletions src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js
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 useItemAuditDataQuery from './useItemAuditDataQuery';

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('useItemAuditDataQuery', () => {
beforeEach(() => {
useOkapiKy.mockClear().mockReturnValue({
get: () => ({
json: () => Promise.resolve({ inventoryAuditItems: [{ action: 'UPDATE' }] }),
}),
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('should fetch item version history', async () => {
const { result } = renderHook(() => useItemAuditDataQuery('itemId'), { wrapper });

await act(() => !result.current.isLoading);

expect(result.current.data).toEqual([{ action: 'UPDATE' }]);
});
});
Loading

0 comments on commit 5093383

Please sign in to comment.