From de7dab90ffad3b16d767436203e0dbbd8b80ebea Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko1 Date: Wed, 26 Feb 2025 21:15:48 +0100 Subject: [PATCH 1/8] UIIN-3175: Item: Display all versions in View history second pane --- CHANGELOG.md | 1 + .../ItemVersionHistory/ItemVersionHistory.js | 109 ++++++++++++++++++ .../ItemVersionHistory.test.js | 0 src/Item/ItemVersionHistory/index.js | 1 + src/hooks/index.js | 1 + src/hooks/useItemAuditDataQuery/index.js | 1 + .../useItemAuditDataQuery.js | 24 ++++ .../useItemAuditDataQuery.test.js | 0 src/views/ItemView.js | 6 +- 9 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/Item/ItemVersionHistory/ItemVersionHistory.js create mode 100644 src/Item/ItemVersionHistory/ItemVersionHistory.test.js create mode 100644 src/Item/ItemVersionHistory/index.js create mode 100644 src/hooks/useItemAuditDataQuery/index.js create mode 100644 src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js create mode 100644 src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3783f6134..bf498e84c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ * Holdings: Suppress action menu and disable buttons when click Change log icon. Refs UIIN-3177. * Item: Suppress action menu and disable buttons when click Change log icon. Refs UIIN-3178. * Use the name CALL_NUMBERS_SHARED for the Shared facet instead of SHARED. Fixes UIIN-3254. +* 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) diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.js b/src/Item/ItemVersionHistory/ItemVersionHistory.js new file mode 100644 index 000000000..06e019507 --- /dev/null +++ b/src/Item/ItemVersionHistory/ItemVersionHistory.js @@ -0,0 +1,109 @@ +import { useContext } from 'react'; +import { useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; + +import { AuditLogPane } from '@folio/stripes-acq-components'; + +import { useItemAuditDataQuery } from '../../hooks'; +import { DataContext } from '../../contexts'; +import { getDateWithTime } from '../../utils'; + +const ItemVersionHistory = ({ + onClose, + itemId, + circulationHistory, +}) => { + const { formatMessage } = useIntl(); + const referenceData = useContext(DataContext); + + const { data, isLoading } = useItemAuditDataQuery(itemId); + + const fieldLabelsMap = { + discoverySuppress: formatMessage({ id: 'ui-inventory.discoverySuppress' }), + callNumber: formatMessage({ id: 'ui-inventory.effectiveCallNumber' }), + prefix: formatMessage({ id: 'ui-inventory.callNumberPrefix' }), + suffix: formatMessage({ id: 'ui-inventory.callNumberSuffix' }), + typeId: formatMessage({ id: 'ui-inventory.callNumberType' }), + accessionNumber: formatMessage({ id: 'ui-inventory.accessionNumber' }), + barcode : formatMessage({ id: 'ui-inventory.itemBarcode' }), + chronology: formatMessage({ id: 'ui-inventory.chronology' }), + copyNumber: formatMessage({ id: 'ui-inventory.copyNumber' }), + descriptionOfPieces: formatMessage({ id: 'ui-inventory.descriptionOfPieces' }), + displaySummary: formatMessage({ id: 'ui-inventory.displaySummary' }), + effectiveLocationId: formatMessage({ id: 'ui-inventory.effectiveLocation' }), + enumeration: formatMessage({ id: 'ui-inventory.enumeration' }), + 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' }), + numberOfMissingPieces: formatMessage({ id: 'ui-inventory.numberOfMissingPieces' }), + numberOfPieces: formatMessage({ id: 'ui-inventory.numberOfPieces' }), + permanentLoanTypeId: formatMessage({ id: 'ui-inventory.permanentLoantype' }), + permanentLocationId: formatMessage({ id: 'ui-inventory.permanentLocation' }), + temporaryLoanTypeId: formatMessage({ id: 'ui-inventory.temporaryLoantype' }), + temporaryLocationId: formatMessage({ id: 'ui-inventory.temporaryLocation' }), + volume: formatMessage({ id: 'ui-inventory.volume' }), + administrativeNotes: formatMessage({ id: 'ui-inventory.administrativeNotes' }), + circulationNotes: formatMessage({ id: 'ui-inventory.circulationHistory' }), + electronicAccess: formatMessage({ id: 'ui-inventory.electronicAccess' }), + formerIds: formatMessage({ id: 'ui-inventory.formerId' }), + notes: formatMessage({ id: 'ui-inventory.itemNotes' }), + statisticalCodeIds: formatMessage({ id: 'ui-inventory.statisticalCodes' }), + yearCaption: formatMessage({ id: 'ui-inventory.yearCaption' }), + dateTime: formatMessage({ id: 'ui-inventory.checkInDate' }), + servicePointId: formatMessage({ id: 'ui-inventory.servicePoint' }), + staffMemberId: formatMessage({ id: 'ui-inventory.source' }), + name: formatMessage({ id: 'ui-inventory.item.availability.itemStatus' }), + date: formatMessage({ id: 'ui-inventory.date' }), + }; + + const fieldFormatter = { + 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}`, + }; + + return ( + + ); +}; + +ItemVersionHistory.propTypes = { + itemId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + circulationHistory: PropTypes.object.isRequired, +}; + +export default ItemVersionHistory; diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.test.js b/src/Item/ItemVersionHistory/ItemVersionHistory.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/Item/ItemVersionHistory/index.js b/src/Item/ItemVersionHistory/index.js new file mode 100644 index 000000000..c1fea79b2 --- /dev/null +++ b/src/Item/ItemVersionHistory/index.js @@ -0,0 +1 @@ +export { default as ItemVersionHistory } from './ItemVersionHistory'; diff --git a/src/hooks/index.js b/src/hooks/index.js index c0d413cbb..d3eab25e8 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -14,6 +14,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 * from './useQuickExport'; export * from '@folio/stripes-inventory-components/lib/queries/useInstanceDateTypes'; export * from './useCallNumberTypesQuery'; diff --git a/src/hooks/useItemAuditDataQuery/index.js b/src/hooks/useItemAuditDataQuery/index.js new file mode 100644 index 000000000..5baed1160 --- /dev/null +++ b/src/hooks/useItemAuditDataQuery/index.js @@ -0,0 +1 @@ +export { default } from './useItemAuditDataQuery'; diff --git a/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js new file mode 100644 index 000000000..3163baf1f --- /dev/null +++ b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js @@ -0,0 +1,24 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; + +const useItemAuditDataQuery = (itemId) => { + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'item-audit-data' }); + + const { isLoading, data = {} } = useQuery({ + queryKey: [namespace, itemId], + queryFn: () => ky.get(`audit-data/inventory/item/${itemId}`).json(), + enabled: Boolean(itemId), + }); + + return { + data: data?.inventoryAuditItems || [], + isLoading, + }; +}; + +export default useItemAuditDataQuery; diff --git a/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/views/ItemView.js b/src/views/ItemView.js index 88707ff04..f8abc3cc7 100644 --- a/src/views/ItemView.js +++ b/src/views/ItemView.js @@ -118,7 +118,7 @@ import { useHoldingMutation, useUpdateOwnership, } from '../hooks'; -import { VersionHistory } from './VersionHistory'; +import { ItemVersionHistory } from '../Item/ItemVersionHistory'; export const requestStatusFiltersString = map(REQUEST_OPEN_STATUSES, requestStatus => `requestStatus.${requestStatus}`).join(','); @@ -1744,8 +1744,10 @@ const ItemView = props => { {isVersionHistoryOpen && ( - setIsSetVersionHistoryOpen(false)} + circulationHistory={circulationHistory} /> )} From 8c4646d3f745a981b155a700e7b0e16c462c8106 Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko1 Date: Wed, 26 Feb 2025 23:26:58 +0100 Subject: [PATCH 2/8] UIIN-3175: Add unit tests --- .../ItemVersionHistory/ItemVersionHistory.js | 52 ++++--- .../ItemVersionHistory.test.js | 144 ++++++++++++++++++ .../useItemAuditDataQuery.test.js | 46 ++++++ 3 files changed, 217 insertions(+), 25 deletions(-) diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.js b/src/Item/ItemVersionHistory/ItemVersionHistory.js index 06e019507..11e3cd47e 100644 --- a/src/Item/ItemVersionHistory/ItemVersionHistory.js +++ b/src/Item/ItemVersionHistory/ItemVersionHistory.js @@ -8,6 +8,32 @@ import { useItemAuditDataQuery } from '../../hooks'; import { DataContext } from '../../contexts'; 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, @@ -63,31 +89,7 @@ const ItemVersionHistory = ({ date: formatMessage({ id: 'ui-inventory.date' }), }; - const fieldFormatter = { - 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 fieldFormatter = createFieldFormatter(referenceData, circulationHistory); return ( ({ + ...jest.requireActual('@folio/stripes-acq-components'), + AuditLogPane: () =>
Version history
, +})); + +jest.mock('../../utils', () => ({ + getDateWithTime: jest.fn(date => `Formatted Date: ${date}`), +})); + +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + useItemAuditDataQuery: jest.fn().mockReturnValue({ data: [{}], isLoading: false }), +})); + +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 = ( + + + + + + ); + + return renderWithIntl(component, translationsProperties); +}; + +describe('ItemVersionHistory', () => { + 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'); + }); +}); diff --git a/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js index e69de29bb..a03f7835d 100644 --- a/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.test.js +++ b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.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 useItemAuditDataQuery from './useItemAuditDataQuery'; + +jest.mock('@folio/stripes/core', () => ({ + ...jest.requireActual('@folio/stripes/core'), + useOkapiKy: jest.fn(), +})); + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + {children} + +); + +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' }]); + }); +}); From 640a0d802065d3a9e658e444179fce563a39f6d4 Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko1 Date: Fri, 28 Feb 2025 13:28:25 +0100 Subject: [PATCH 3/8] UIIN-3175: Make createVersionHistoryFieldFormatter common for holdings and items --- package.json | 2 +- .../ItemVersionHistory/ItemVersionHistory.js | 30 +----- .../ItemVersionHistory.test.js | 92 +------------------ src/utils.js | 31 +++++++ src/utils.test.js | 90 ++++++++++++++++++ 5 files changed, 126 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index f55d6dfd3..859ee4497 100644 --- a/package.json +++ b/package.json @@ -1072,7 +1072,7 @@ "zustand": "^4.1.1" }, "dependencies": { - "@folio/stripes-acq-components": "~6.0.0", + "@folio/stripes-acq-components": "https://github.com/folio-org/stripes-acq-components.git#UISACQCOMP-248", "classnames": "^2.3.2", "file-saver": "^2.0.0", "final-form": "^4.18.2", diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.js b/src/Item/ItemVersionHistory/ItemVersionHistory.js index 11e3cd47e..99fd0b6c8 100644 --- a/src/Item/ItemVersionHistory/ItemVersionHistory.js +++ b/src/Item/ItemVersionHistory/ItemVersionHistory.js @@ -6,33 +6,7 @@ import { AuditLogPane } from '@folio/stripes-acq-components'; import { useItemAuditDataQuery } from '../../hooks'; import { DataContext } from '../../contexts'; -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}`, -}); +import { createVersionHistoryFieldFormatter } from '../../utils'; const ItemVersionHistory = ({ onClose, @@ -89,7 +63,7 @@ const ItemVersionHistory = ({ date: formatMessage({ id: 'ui-inventory.date' }), }; - const fieldFormatter = createFieldFormatter(referenceData, circulationHistory); + const fieldFormatter = createVersionHistoryFieldFormatter(referenceData, circulationHistory); return ( ({ ...jest.requireActual('@folio/stripes-acq-components'), @@ -30,26 +30,11 @@ 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 = ( - + { 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'); - }); -}); diff --git a/src/utils.js b/src/utils.js index 70697919b..8fcd845ec 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1123,3 +1123,34 @@ export const omitCurrentAndCentralTenants = (stripes) => { return tenants?.filter(tenant => tenant.id !== currentTenantId && tenant.id !== centralTenantId); }; + +export const createVersionHistoryFieldFormatter = (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}`, + holdingsTypeId: value => referenceData.holdingsTypes?.find(type => type.id === value)?.name, + callNumberTypeId: value => referenceData.callNumberTypes?.find(type => type.id === value)?.name, + illPolicyId: value => referenceData.illPolicies.find(policy => policy.id === value)?.name, + holdingsNoteTypeId: value => referenceData.holdingsNoteTypes?.find(noteType => noteType.id === value)?.name, + publicDisplay: value => value.toString(), +}); diff --git a/src/utils.test.js b/src/utils.test.js index ea49de9cc..a31305751 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -22,6 +22,7 @@ import { checkIfCentralOrderingIsActive, omitCurrentAndCentralTenants, marshalInstance, + createVersionHistoryFieldFormatter, } from './utils'; import { CONTENT_TYPE_HEADER, @@ -490,3 +491,92 @@ describe('hasMemberTenantPermission', () => { }); }); }); + +describe('createVersionHistoryFieldFormatter', () => { + 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 fieldFormatter = createVersionHistoryFieldFormatter(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'); + }); +}); From 98fd0e0325820e1f3323a8cd2586e685e356c07b Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko <85172747+OleksandrHladchenko1@users.noreply.github.com> Date: Fri, 28 Feb 2025 13:33:26 +0100 Subject: [PATCH 4/8] Update utils.test.js --- src/utils.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.test.js b/src/utils.test.js index ab122f0fe..f570a070c 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -506,6 +506,7 @@ describe('getIsVersionHistoryEnabled', () => { expect(getIsVersionHistoryEnabled(settings)).toBe(false); }); +}); describe('createVersionHistoryFieldFormatter', () => { const date = '2024-02-26T12:00:00Z'; From cbf12a851490462a349b0d12bb9f661514b85184 Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko1 Date: Fri, 28 Feb 2025 15:36:54 +0100 Subject: [PATCH 5/8] UIIN-3175: Revert changes --- .../ItemVersionHistory/ItemVersionHistory.js | 30 ++++++- .../ItemVersionHistory.test.js | 90 ++++++++++++++++++- src/utils.js | 31 ------- src/utils.test.js | 90 ------------------- 4 files changed, 117 insertions(+), 124 deletions(-) diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.js b/src/Item/ItemVersionHistory/ItemVersionHistory.js index 99fd0b6c8..11e3cd47e 100644 --- a/src/Item/ItemVersionHistory/ItemVersionHistory.js +++ b/src/Item/ItemVersionHistory/ItemVersionHistory.js @@ -6,7 +6,33 @@ import { AuditLogPane } from '@folio/stripes-acq-components'; import { useItemAuditDataQuery } from '../../hooks'; import { DataContext } from '../../contexts'; -import { createVersionHistoryFieldFormatter } from '../../utils'; +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, @@ -63,7 +89,7 @@ const ItemVersionHistory = ({ date: formatMessage({ id: 'ui-inventory.date' }), }; - const fieldFormatter = createVersionHistoryFieldFormatter(referenceData, circulationHistory); + const fieldFormatter = createFieldFormatter(referenceData, circulationHistory); return ( ({ ...jest.requireActual('@folio/stripes-acq-components'), @@ -30,6 +30,21 @@ 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 = ( @@ -54,3 +69,76 @@ describe('ItemVersionHistory', () => { 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'); + }); +}); diff --git a/src/utils.js b/src/utils.js index a756c22e6..47430e20f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1128,34 +1128,3 @@ export const omitCurrentAndCentralTenants = (stripes) => { export const getIsVersionHistoryEnabled = settings => { return settings?.find(setting => setting.key === VERSION_HISTORY_ENABLED_SETTING)?.value; }; - -export const createVersionHistoryFieldFormatter = (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}`, - holdingsTypeId: value => referenceData.holdingsTypes?.find(type => type.id === value)?.name, - callNumberTypeId: value => referenceData.callNumberTypes?.find(type => type.id === value)?.name, - illPolicyId: value => referenceData.illPolicies.find(policy => policy.id === value)?.name, - holdingsNoteTypeId: value => referenceData.holdingsNoteTypes?.find(noteType => noteType.id === value)?.name, - publicDisplay: value => value.toString(), -}); diff --git a/src/utils.test.js b/src/utils.test.js index f570a070c..cbf26ff8f 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -23,7 +23,6 @@ import { omitCurrentAndCentralTenants, marshalInstance, getIsVersionHistoryEnabled, - createVersionHistoryFieldFormatter, } from './utils'; import { CONTENT_TYPE_HEADER, @@ -507,92 +506,3 @@ describe('getIsVersionHistoryEnabled', () => { expect(getIsVersionHistoryEnabled(settings)).toBe(false); }); }); - -describe('createVersionHistoryFieldFormatter', () => { - 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 fieldFormatter = createVersionHistoryFieldFormatter(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'); - }); -}); From e315f71548cea4abf107cb20085c3757671a2a16 Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko1 Date: Fri, 28 Feb 2025 15:59:37 +0100 Subject: [PATCH 6/8] UIIN-3175: Use AuditLogPane from streipes/components --- .../ItemVersionHistory/ItemVersionHistory.js | 34 +++++- src/hooks/index.js | 1 + .../useItemAuditDataQuery.js | 12 +- src/hooks/useVersionHistory/getActionLabel.js | 12 ++ .../useVersionHistory/getActionLabel.test.js | 15 +++ .../useVersionHistory/getChangedFieldsList.js | 28 +++++ src/hooks/useVersionHistory/index.js | 1 + .../useVersionHistory/useVersionHistory.js | 104 ++++++++++++++++++ 8 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useVersionHistory/getActionLabel.js create mode 100644 src/hooks/useVersionHistory/getActionLabel.test.js create mode 100644 src/hooks/useVersionHistory/getChangedFieldsList.js create mode 100644 src/hooks/useVersionHistory/index.js create mode 100644 src/hooks/useVersionHistory/useVersionHistory.js diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.js b/src/Item/ItemVersionHistory/ItemVersionHistory.js index 11e3cd47e..4d1d78bb2 100644 --- a/src/Item/ItemVersionHistory/ItemVersionHistory.js +++ b/src/Item/ItemVersionHistory/ItemVersionHistory.js @@ -1,10 +1,13 @@ -import { useContext } from 'react'; +import { useContext, useState } from 'react'; import { useIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import { AuditLogPane } from '@folio/stripes-acq-components'; +import { AuditLogPane } from '@folio/stripes/components'; -import { useItemAuditDataQuery } from '../../hooks'; +import { + useItemAuditDataQuery, + useVersionHistory, +} from '../../hooks'; import { DataContext } from '../../contexts'; import { getDateWithTime } from '../../utils'; @@ -42,7 +45,19 @@ const ItemVersionHistory = ({ const { formatMessage } = useIntl(); const referenceData = useContext(DataContext); - const { data, isLoading } = useItemAuditDataQuery(itemId); + const [lastVersionEventTs, setLastVersionEventTs] = useState(null); + + const { + data, + totalRecords, + isLoading, + } = useItemAuditDataQuery(itemId, lastVersionEventTs); + + const { + actionsMap, + isLoadedMoreVisible, + versionsToDisplay, + } = useVersionHistory(data, totalRecords); const fieldLabelsMap = { discoverySuppress: formatMessage({ id: 'ui-inventory.discoverySuppress' }), @@ -91,13 +106,20 @@ const ItemVersionHistory = ({ const fieldFormatter = createFieldFormatter(referenceData, circulationHistory); + const handleLoadMore = lastEventTs => { + setLastVersionEventTs(lastEventTs); + }; + return ( ); }; diff --git a/src/hooks/index.js b/src/hooks/index.js index d3eab25e8..173cf265f 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -15,6 +15,7 @@ export { default as useClassificationBrowseConfig } from './useClassificationBro export { default as useUpdateOwnership } from './useUpdateOwnership'; export { default as useLocalStorageItems } from './useLocalStorageItems'; export { default as useItemAuditDataQuery } from './useItemAuditDataQuery'; +export { default as useVersionHistory } from './useVersionHistory'; export * from './useQuickExport'; export * from '@folio/stripes-inventory-components/lib/queries/useInstanceDateTypes'; export * from './useCallNumberTypesQuery'; diff --git a/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js index 3163baf1f..1d9d627d4 100644 --- a/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js +++ b/src/hooks/useItemAuditDataQuery/useItemAuditDataQuery.js @@ -5,18 +5,24 @@ import { useOkapiKy, } from '@folio/stripes/core'; -const useItemAuditDataQuery = (itemId) => { +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], - queryFn: () => ky.get(`audit-data/inventory/item/${itemId}`).json(), + 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, }; }; diff --git a/src/hooks/useVersionHistory/getActionLabel.js b/src/hooks/useVersionHistory/getActionLabel.js new file mode 100644 index 000000000..c91228afb --- /dev/null +++ b/src/hooks/useVersionHistory/getActionLabel.js @@ -0,0 +1,12 @@ +/** + * Gets translated change type label + * @param {function} formatMessage + * @returns {{ADDED, MODIFIED, REMOVED}} + */ +export const getActionLabel = formatMessage => { + return { + ADDED: formatMessage({ id: 'stripes-acq-components.audit-log.action.added' }), + MODIFIED: formatMessage({ id: 'stripes-acq-components.audit-log.action.edited' }), + REMOVED: formatMessage({ id: 'stripes-acq-components.audit-log.action.removed' }), + }; +}; diff --git a/src/hooks/useVersionHistory/getActionLabel.test.js b/src/hooks/useVersionHistory/getActionLabel.test.js new file mode 100644 index 000000000..444a7c5ac --- /dev/null +++ b/src/hooks/useVersionHistory/getActionLabel.test.js @@ -0,0 +1,15 @@ +import { getActionLabel } from './getActionLabel'; + +const intl = { formatMessage: ({ id }) => id }; + +describe('getActionLabel', () => { + it('should return correct action labels', () => { + const labels = { + ADDED: 'stripes-acq-components.audit-log.action.added', + MODIFIED: 'stripes-acq-components.audit-log.action.edited', + REMOVED: 'stripes-acq-components.audit-log.action.removed', + }; + + expect(getActionLabel(intl.formatMessage)).toEqual(labels); + }); +}); diff --git a/src/hooks/useVersionHistory/getChangedFieldsList.js b/src/hooks/useVersionHistory/getChangedFieldsList.js new file mode 100644 index 000000000..d6c33b7f9 --- /dev/null +++ b/src/hooks/useVersionHistory/getChangedFieldsList.js @@ -0,0 +1,28 @@ +import { sortBy } from 'lodash'; + +/** + * Merge fieldChanges and collectionChanges into a list of changed fields and sort by changeType + * @param {Object} diff + * @param {Array} diff.fieldChanges + * @param {Array} diff.collectionChanges + * @returns {Array.<{fieldName: String, changeType: String, newValue: any, oldValue: any}>} + */ +export const getChangedFieldsList = diff => { + const fieldChanges = diff.fieldChanges ? diff.fieldChanges.map(field => ({ + fieldName: field.fieldName, + changeType: field.changeType, + newValue: field.newValue, + oldValue: field.oldValue, + })) : []; + + const collectionChanges = diff.collectionChanges ? diff.collectionChanges.flatMap(collection => { + return collection.itemChanges.map(field => ({ + fieldName: collection.collectionName, + changeType: field.changeType, + newValue: field.newValue, + oldValue: field.oldValue, + })); + }) : []; + + return sortBy([...fieldChanges, ...collectionChanges], data => data.changeType); +}; diff --git a/src/hooks/useVersionHistory/index.js b/src/hooks/useVersionHistory/index.js new file mode 100644 index 000000000..af5a0400f --- /dev/null +++ b/src/hooks/useVersionHistory/index.js @@ -0,0 +1 @@ +export { default } from './useVersionHistory'; diff --git a/src/hooks/useVersionHistory/useVersionHistory.js b/src/hooks/useVersionHistory/useVersionHistory.js new file mode 100644 index 000000000..4ba1eb1da --- /dev/null +++ b/src/hooks/useVersionHistory/useVersionHistory.js @@ -0,0 +1,104 @@ +import { + useEffect, + useMemo, + useState, +} from 'react'; +import { useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; +import { + keyBy, + uniq, +} from 'lodash'; + +import { + formatDateTime, + useUsersBatch, +} from '@folio/stripes-acq-components'; + +import { getChangedFieldsList } from './getChangedFieldsList'; +import { getActionLabel } from './getActionLabel'; + +const useVersionHistory = (data, totalRecords) => { + const intl = useIntl(); + const anonymousUserLabel = intl.formatMessage({ id: 'stripes-components.versionHistory.anonymousUser' }); + + const [versions, setVersions] = useState([]); + const [usersId, setUsersId] = useState([]); + const [usersMap, setUsersMap] = useState({}); + const [isLoadedMoreVisible, setIsLoadedMoreVisible] = useState(true); + + const { users } = useUsersBatch(usersId); + + // cleanup when component unmounts + useEffect(() => () => { + setVersions([]); + setUsersMap({}); + }, []); + + // update usersId when data changes + useEffect(() => { + if (!data?.length) return; + + const newUsersId = uniq(data.map(version => version.userId)); + + setUsersId(newUsersId); + }, [data]); + + // update usersMap when new users are fetched + useEffect(() => { + if (!users?.length) return; + + setUsersMap(prevState => ({ + ...prevState, + ...keyBy(users, 'id'), + })); + }, [users]); + + useEffect(() => { + if (!data?.length) return; + + setVersions(prevState => [...prevState, ...data]); + }, [data]); + + useEffect(() => { + setIsLoadedMoreVisible(versions.length < totalRecords); + }, [versions]); + + const versionsToDisplay = useMemo( + () => { + const getUserName = userId => { + const user = usersMap[userId]; + + return user ? `${user.personal.lastName}, ${user.personal.firstName}` : null; + }; + const getSourceLink = userId => { + return userId ? {getUserName(userId)} : anonymousUserLabel; + }; + + const transformDiffToVersions = diffArray => { + return diffArray + .filter(({ action }) => action !== 'CREATE') + .map(({ eventDate, eventTs, userId, eventId, diff }) => ({ + eventDate: formatDateTime(eventDate, intl), + source: getSourceLink(userId), + userName: getUserName(userId) || anonymousUserLabel, + fieldChanges: diff ? getChangedFieldsList(diff) : [], + eventId, + eventTs, + })); + }; + + return transformDiffToVersions(versions); + }, [versions, usersMap], + ); + + const actionsMap = { ...getActionLabel(intl.formatMessage) }; + + return { + actionsMap, + isLoadedMoreVisible, + versionsToDisplay, + }; +}; + +export default useVersionHistory; From 42052c413859a14a9c1902f9f674bed4c0b82501 Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko1 Date: Fri, 28 Feb 2025 16:13:45 +0100 Subject: [PATCH 7/8] UIIN-3175: Update mock --- src/Item/ItemVersionHistory/ItemVersionHistory.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Item/ItemVersionHistory/ItemVersionHistory.test.js b/src/Item/ItemVersionHistory/ItemVersionHistory.test.js index 4b5ba1625..11ff01ab5 100644 --- a/src/Item/ItemVersionHistory/ItemVersionHistory.test.js +++ b/src/Item/ItemVersionHistory/ItemVersionHistory.test.js @@ -12,8 +12,8 @@ import { import { DataContext } from '../../contexts'; import ItemVersionHistory, { createFieldFormatter } from './ItemVersionHistory'; -jest.mock('@folio/stripes-acq-components', () => ({ - ...jest.requireActual('@folio/stripes-acq-components'), +jest.mock('@folio/stripes/components', () => ({ + ...jest.requireActual('@folio/stripes/components'), AuditLogPane: () =>
Version history
, })); From c0dafaa047cdc8b0a2bb491c35c59b1eaecca8b1 Mon Sep 17 00:00:00 2001 From: Oleksandr Hladchenko <85172747+OleksandrHladchenko1@users.noreply.github.com> Date: Fri, 28 Feb 2025 16:43:30 +0100 Subject: [PATCH 8/8] Update ItemView.js --- src/views/ItemView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/ItemView.js b/src/views/ItemView.js index ce71c6804..c44103f8b 100644 --- a/src/views/ItemView.js +++ b/src/views/ItemView.js @@ -1034,7 +1034,7 @@ const ItemView = props => { lastMenu={( {showVersionHistoryButton && ( -