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;