Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIIN-3175: Item: Display all versions in View history second pane #2763

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
* Update Inventory: Set records for deletion permission's effect on ‘Set for deletion’ checkbox in Instance Edit view. Refs UIIN-3260.
* *BREAKING* Migrate stripes dependencies to their Sunflower versions. Refs UIIN-3223.
* *BREAKING* Migrate `react-intl` to v7. Refs UIIN-3224.
* 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
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 {
useItemAuditDataQuery,
useVersionHistory,
} 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,
circulationHistory,
}) => {
const { formatMessage } = useIntl();
const referenceData = useContext(DataContext);

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' }),
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 = createFieldFormatter(referenceData, circulationHistory);

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

return (
<AuditLogPane
versions={versionsToDisplay}
onClose={onClose}
isLoadedMoreVisible={isLoadedMoreVisible}
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;
144 changes: 144 additions & 0 deletions src/Item/ItemVersionHistory/ItemVersionHistory.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {
QueryClient,
QueryClientProvider,
} from 'react-query';
import { screen } from '@folio/jest-config-stripes/testing-library/react';

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

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

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 }),
}));

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 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';
2 changes: 2 additions & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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 useVersionHistory } from './useVersionHistory';
export * from './useQuickExport';
export * from '@folio/stripes-inventory-components/lib/queries/useInstanceDateTypes';
export * from './useCallNumberTypesQuery';
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' }]);
});
});
12 changes: 12 additions & 0 deletions src/hooks/useVersionHistory/getActionLabel.js
Original file line number Diff line number Diff line change
@@ -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' }),
};
};
Loading
Loading