From 0c4574b781b7635d0b277071b94063f69b2ed3ac Mon Sep 17 00:00:00 2001 From: Mariia_Aloshyna Date: Mon, 24 Feb 2025 15:52:20 +0200 Subject: [PATCH 1/4] UIIN-3173: Instance: Display all versions in View history fourth pane --- CHANGELOG.md | 1 + .../InstanceDetails/InstanceDetails.js | 8 +- .../InstanceVersionHistory.js | 103 ++++++++++++++++++ .../InstanceVersionHistory.test.js | 65 +++++++++++ src/Instance/InstanceVersionHistory/index.js | 1 + src/hooks/index.js | 1 + src/hooks/useInstanceAuditDataQuery/index.js | 1 + .../useInstanceAuditDataQuery.js | 26 +++++ 8 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 src/Instance/InstanceVersionHistory/InstanceVersionHistory.js create mode 100644 src/Instance/InstanceVersionHistory/InstanceVersionHistory.test.js create mode 100644 src/Instance/InstanceVersionHistory/index.js create mode 100644 src/hooks/useInstanceAuditDataQuery/index.js create mode 100644 src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cfdd1d44..84faf7fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * Add Version history button and Version history pane to details view of Instance. Refs UIIN-3170. * Add new ‘Set for deletion’ flag to display on 3rd pane Instance view. Refs UIIN-3191. * Add settings options for using number gernerator for item barcode, accession number and call number. Refs UIIN-2557. +* Instance: Display all versions in View history fourth pane. Refs UIIN-3173. ## [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/Instance/InstanceDetails/InstanceDetails.js b/src/Instance/InstanceDetails/InstanceDetails.js index 9926d5871..cd4ee6ca2 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.js +++ b/src/Instance/InstanceDetails/InstanceDetails.js @@ -43,11 +43,12 @@ import { InstanceClassificationView } from './InstanceClassificationView'; import { InstanceRelationshipView } from './InstanceRelationshipView'; import { InstanceNewHolding } from './InstanceNewHolding'; import { InstanceAcquisition } from './InstanceAcquisition'; -import HelperApp from '../../components/HelperApp'; -import { VersionHistory } from '../../views/VersionHistory'; +import HelperApp from '../../components/HelperApp'; import { DataContext } from '../../contexts'; import { ConsortialHoldings } from '../HoldingsList/consortium/ConsortialHoldings'; +import { InstanceVersionHistory } from '../InstanceVersionHistory'; + import { getAccordionState, getPublishingInfo, @@ -328,7 +329,8 @@ const InstanceDetails = forwardRef(({ {isVersionHistoryOpen && ( - setIsVersionHistoryOpen(false)} /> )} diff --git a/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js new file mode 100644 index 000000000..ec98fdcf0 --- /dev/null +++ b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js @@ -0,0 +1,103 @@ +import { useContext } from 'react'; +import { useIntl } from 'react-intl'; +import PropTypes from 'prop-types'; + +import { NoValue } from '@folio/stripes/components'; +import { AuditLogPane } from '@folio/stripes-acq-components'; + +import { DataContext } from '../../contexts'; + +import { useInstanceAuditDataQuery } from '../../hooks'; +import { getDateWithTime } from '../../utils'; + +const InstanceVersionHistory = ({ + instanceId, + onClose, +}) => { + const { formatMessage } = useIntl(); + const { data, isLoading } = useInstanceAuditDataQuery(instanceId); + const referenceData = useContext(DataContext); + + const fieldLabelsMap = { + administrativeNotes: formatMessage({ id: 'ui-inventory.administrativeNotes' }), + alternativeTitles: formatMessage({ id: 'ui-inventory.alternativeTitles' }), + catalogedDate: formatMessage({ id: 'ui-inventory.catalogedDate' }), + childInstances: formatMessage({ id: 'ui-inventory.childInstances' }), + classifications: formatMessage({ id: 'ui-inventory.classifications' }), + contributors: formatMessage({ id: 'ui-inventory.contributors' }), + date1: formatMessage({ id: 'ui-inventory.date1' }), + date2: formatMessage({ id: 'ui-inventory.date2' }), + dateTypeId: formatMessage({ id: 'ui-inventory.dateType' }), + deleted: formatMessage({ id: 'ui-inventory.setForDeletion' }), + discoverySuppress: formatMessage({ id: 'ui-inventory.discoverySuppressed' }), + editions: formatMessage({ id: 'ui-inventory.edition' }), + electronicAccess: formatMessage({ id: 'ui-inventory.electronicAccess' }), + hrid: formatMessage({ id: 'ui-inventory.instanceHrid' }), + identifiers: formatMessage({ id: 'ui-inventory.identifiers' }), + instanceFormatIds: formatMessage({ id: 'ui-inventory.instanceFormats' }), + instanceTypeId: formatMessage({ id: 'ui-inventory.resourceType' }), + indexTitle: formatMessage({ id: 'ui-inventory.indexTitle' }), + languages: formatMessage({ id: 'ui-inventory.languages' }), + modeOfIssuanceId: formatMessage({ id: 'ui-inventory.modeOfIssuance' }), + natureOfContentTermIds: formatMessage({ id: 'ui-inventory.natureOfContentTerms' }), + notes: formatMessage({ id: 'ui-inventory.instanceNotes' }), + parentInstances: formatMessage({ id: 'ui-inventory.parentInstances' }), + physicalDescriptions: formatMessage({ id: 'ui-inventory.physicalDescriptions' }), + precedingTitles: formatMessage({ id: 'ui-inventory.precedingTitles' }), + previouslyHeld: formatMessage({ id: 'ui-inventory.previouslyHeld' }), + publication: formatMessage({ id: 'ui-inventory.publications' }), + publicationFrequency: formatMessage({ id: 'ui-inventory.publicationFrequency' }), + publicationRange: formatMessage({ id: 'ui-inventory.publicationRange' }), + series: formatMessage({ id: 'ui-inventory.seriesStatements' }), + source: formatMessage({ id: 'ui-inventory.source' }), + staffSuppress: formatMessage({ id: 'ui-inventory.staffSuppressed' }), + statisticalCodeIds: formatMessage({ id: 'ui-inventory.statisticalCodes' }), + statusId: formatMessage({ id: 'ui-inventory.instanceStatus' }), + statusUpdatedDate: formatMessage({ id: 'ui-inventory.instanceStatusUpdatedDate' }), + subjects: formatMessage({ id: 'ui-inventory.subjects' }), + succeedingTitles: formatMessage({ id: 'ui-inventory.succeedingTitles' }), + tagList: formatMessage({ id: 'stripes-smart-components.tags' }), + title: formatMessage({ id: 'ui-inventory.instances.columns.title' }), + }; + const fieldFormatter = { + alternativeTitleTypeId: value => referenceData.alternativeTitleTypes?.find(type => type.id === value)?.name, + classificationTypeId: value => referenceData.classificationTypes?.find(type => type.id === value)?.name, + contributorNameTypeId: value => referenceData.contributorNameTypes?.find(contributor => contributor.id === value)?.name, + contributorTypeId: value => referenceData.contributorTypes?.find(contributor => contributor.id === value)?.name, + contributorTypeText: value => value || , + dateTypeId: value => referenceData.instanceDateTypes?.find(type => type.id === value)?.name, + identifierTypeId: value => referenceData.identifierTypes?.find(type => type.id === value)?.name, + instanceFormatIds: value => referenceData.instanceFormats?.find(format => format.id === value)?.name, + instanceNoteTypeId: value => referenceData.instanceNoteTypes?.find(note => note.id === value)?.name, + instanceTypeId: value => referenceData.instanceTypes?.find(type => type.id === value)?.name, + modeOfIssuanceId: value => referenceData.modesOfIssuance?.find(mode => mode.id === value)?.name, + natureOfContentTermIds: value => referenceData.natureOfContentTerms?.find(term => term.id === value)?.name, + primary: value => value.toString(), + relationshipId: value => referenceData.electronicAccessRelationships?.find(rel => rel.id === value)?.name, + sourceId: value => referenceData.subjectSources?.find(source => source.id === value)?.name, + staffOnly: value => value.toString(), + statisticalCodeIds: value => referenceData.statisticalCodes?.find(code => code.id === value)?.name, + statusId: value => referenceData.instanceStatuses?.find(status => status.id === value)?.name, + statusUpdatedDate: value => getDateWithTime(value), + typeId: value => referenceData.subjectTypes?.find(type => type.id === value)?.name, + uri: value => value || , + }; + + + return ( + + ); +}; + +InstanceVersionHistory.propTypes = { + instanceId: PropTypes.string.isRequired, + onClose: PropTypes.func, +}; + +export default InstanceVersionHistory; diff --git a/src/Instance/InstanceVersionHistory/InstanceVersionHistory.test.js b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.test.js new file mode 100644 index 000000000..e73e51ea6 --- /dev/null +++ b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.test.js @@ -0,0 +1,65 @@ +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; +import { screen } from '@folio/jest-config-stripes/testing-library/react'; + +import { + renderWithIntl, + translationsProperties, +} from '../../../test/jest/helpers'; + +import InstanceVersionHistory from './InstanceVersionHistory'; +import { DataContext } from '../../contexts'; + +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + useInstanceAuditDataQuery: () => jest.fn(), +})); + +const queryClient = new QueryClient(); + +const onCloseMock = jest.fn(); +const instanceId = 'instanceId'; +const mockReferenceData = { + alternativeTitleTypes: [], + classificationTypes: [], + contributorNameTypes: [], + contributorTypes: [], + instanceDateTypes: [], + identifierTypes: [], + instanceFormats: [], + instanceNoteTypes: [], + instanceTypes: [], + modesOfIssuance: [], + natureOfContentTerms: [], + electronicAccessRelationships: [], + subjectSources: [], + statisticalCodes: [], + instanceStatuses: [], + subjectTypes:[], +}; + +const renderInstanceVersionHistory = () => { + const component = ( + + + + + + ); + + return renderWithIntl(component, translationsProperties); +}; + +describe('InstanceVersionHistory', () => { + it('should render View history pane', () => { + renderInstanceVersionHistory(); + + expect(screen.getByText('Version history')).toBeInTheDocument(); + }); +}); + diff --git a/src/Instance/InstanceVersionHistory/index.js b/src/Instance/InstanceVersionHistory/index.js new file mode 100644 index 000000000..40910fc65 --- /dev/null +++ b/src/Instance/InstanceVersionHistory/index.js @@ -0,0 +1 @@ +export { default as InstanceVersionHistory } from './InstanceVersionHistory'; diff --git a/src/hooks/index.js b/src/hooks/index.js index eb789b180..be87c0617 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -5,6 +5,7 @@ export { default as useCallout } from './useCallout'; export { default as useHoldingItemsQuery } from './useHoldingItemsQuery'; export { default as useHoldingMutation } from './useHoldingMutation'; export { default as useHoldingsFromStorage } from './useHoldingsFromStorage'; +export { default as useInstanceAuditDataQuery } from './useInstanceAuditDataQuery'; export { default as useInstanceMutation } from './useInstanceMutation'; export { default as useHoldingsQueryByHrids } from './useHoldingsQueryByHrids'; export { default as useInventoryBrowse } from './useInventoryBrowse'; diff --git a/src/hooks/useInstanceAuditDataQuery/index.js b/src/hooks/useInstanceAuditDataQuery/index.js new file mode 100644 index 000000000..ecf93d4ee --- /dev/null +++ b/src/hooks/useInstanceAuditDataQuery/index.js @@ -0,0 +1 @@ +export { default } from './useInstanceAuditDataQuery'; diff --git a/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js b/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js new file mode 100644 index 000000000..9b10651bb --- /dev/null +++ b/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js @@ -0,0 +1,26 @@ +import { useQuery } from 'react-query'; + +import { + useNamespace, + useOkapiKy, +} from '@folio/stripes/core'; + +import { LIMIT_MAX } from '@folio/stripes-inventory-components'; + +const useInstanceAuditDataQuery = (instanceId) => { + const ky = useOkapiKy(); + const [namespace] = useNamespace({ key: 'instance-audit-data' }); + + const { isLoading, data = {} } = useQuery({ + queryKey: [namespace, instanceId], + queryFn: () => ky.get(`audit-data/inventory/instance/${instanceId}`, { searchParams: { limit: LIMIT_MAX } }).json(), + enabled: Boolean(instanceId), + }); + + return { + data: data?.inventoryAuditItems || [], + isLoading, + }; +}; + +export default useInstanceAuditDataQuery; From 5917423caa47be74612888907fbbd9acbc064fcf Mon Sep 17 00:00:00 2001 From: Mariia_Aloshyna Date: Mon, 24 Feb 2025 18:02:40 +0200 Subject: [PATCH 2/4] add tests --- .../useInstanceAuditDataQuery/useInstanceAuditDataQuery.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js b/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js index 9b10651bb..cc8aa388e 100644 --- a/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js +++ b/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js @@ -13,7 +13,12 @@ const useInstanceAuditDataQuery = (instanceId) => { const { isLoading, data = {} } = useQuery({ queryKey: [namespace, instanceId], - queryFn: () => ky.get(`audit-data/inventory/instance/${instanceId}`, { searchParams: { limit: LIMIT_MAX } }).json(), + queryFn: () => ky.get(`audit-data/inventory/instance/${instanceId}`, { + searchParams: { + limit: LIMIT_MAX, + offset: 0, + } + }).json(), enabled: Boolean(instanceId), }); From 3b701bb6491880c17d87dbb75cb328356d774253 Mon Sep 17 00:00:00 2001 From: Mariia_Aloshyna Date: Wed, 26 Feb 2025 13:19:57 +0200 Subject: [PATCH 3/4] fix sonar issues --- .../InstanceVersionHistory.js | 32 +++++++++++++++++-- .../useInstanceAuditDataQuery.js | 11 +++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js index ec98fdcf0..8c1d88dcc 100644 --- a/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js +++ b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js @@ -1,4 +1,8 @@ -import { useContext } from 'react'; +import { + useContext, + useEffect, + useState, +} from 'react'; import { useIntl } from 'react-intl'; import PropTypes from 'prop-types'; @@ -15,9 +19,29 @@ const InstanceVersionHistory = ({ onClose, }) => { const { formatMessage } = useIntl(); - const { data, isLoading } = useInstanceAuditDataQuery(instanceId); + const [lastVersionEventTs, setLastVersionEventTs] = useState(null); + const { + data, + totalRecords, + isLoading, + } = useInstanceAuditDataQuery(instanceId, lastVersionEventTs); + // const [versions, setVersions] = useState([]); + const [isLoadedMoreVisible, setIsLoadedMoreVisible] = useState(true); const referenceData = useContext(DataContext); + // display more cards if loaded more + // useEffect(() => { + // if (data?.length) { + // setVersions(prevState => [...prevState, ...data]); + // } + // }, [data]); + + // useEffect(() => { + // setIsLoadedMoreVisible(Boolean(versions.length !== 0 && versions.length < totalRecords)); + // }, [versions, totalRecords]); + + // useEffect(() => () => setVersions([]), []); + const fieldLabelsMap = { administrativeNotes: formatMessage({ id: 'ui-inventory.administrativeNotes' }), alternativeTitles: formatMessage({ id: 'ui-inventory.alternativeTitles' }), @@ -86,9 +110,11 @@ const InstanceVersionHistory = ({ return ( diff --git a/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js b/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js index cc8aa388e..2c6b34382 100644 --- a/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js +++ b/src/hooks/useInstanceAuditDataQuery/useInstanceAuditDataQuery.js @@ -5,18 +5,16 @@ import { useOkapiKy, } from '@folio/stripes/core'; -import { LIMIT_MAX } from '@folio/stripes-inventory-components'; - -const useInstanceAuditDataQuery = (instanceId) => { +const useInstanceAuditDataQuery = (instanceId, eventTs) => { const ky = useOkapiKy(); const [namespace] = useNamespace({ key: 'instance-audit-data' }); + // eventTs param is used to load more data const { isLoading, data = {} } = useQuery({ - queryKey: [namespace, instanceId], + queryKey: [namespace, instanceId, eventTs], queryFn: () => ky.get(`audit-data/inventory/instance/${instanceId}`, { searchParams: { - limit: LIMIT_MAX, - offset: 0, + ...(eventTs && { eventTs }) } }).json(), enabled: Boolean(instanceId), @@ -24,6 +22,7 @@ const useInstanceAuditDataQuery = (instanceId) => { return { data: data?.inventoryAuditItems || [], + totalRecords: data?.totalRecords, isLoading, }; }; From a71514d280efa94f67bef35abbc8391e81d117a9 Mon Sep 17 00:00:00 2001 From: Mariia_Aloshyna Date: Fri, 28 Feb 2025 16:23:18 +0200 Subject: [PATCH 4/4] UIIN-3173: Instance: Display all versions in View history fourth pane --- .../InstanceVersionHistory.js | 47 ++++---- src/hooks/index.js | 1 + 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 ++++++++++++++++++ 7 files changed, 186 insertions(+), 22 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/Instance/InstanceVersionHistory/InstanceVersionHistory.js b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js index 8c1d88dcc..e867d4c2d 100644 --- a/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js +++ b/src/Instance/InstanceVersionHistory/InstanceVersionHistory.js @@ -1,46 +1,45 @@ import { useContext, - useEffect, useState, } from 'react'; import { useIntl } from 'react-intl'; import PropTypes from 'prop-types'; -import { NoValue } from '@folio/stripes/components'; -import { AuditLogPane } from '@folio/stripes-acq-components'; +import { + AuditLogPane, + NoValue, +} from '@folio/stripes/components'; import { DataContext } from '../../contexts'; +import { + useInstanceAuditDataQuery, + useVersionHistory, +} from '../../hooks'; -import { useInstanceAuditDataQuery } from '../../hooks'; import { getDateWithTime } from '../../utils'; const InstanceVersionHistory = ({ instanceId, onClose, }) => { - const { formatMessage } = useIntl(); + const intl = useIntl(); + const { formatMessage } = intl; + + const referenceData = useContext(DataContext); + const [lastVersionEventTs, setLastVersionEventTs] = useState(null); + const { data, totalRecords, isLoading, } = useInstanceAuditDataQuery(instanceId, lastVersionEventTs); - // const [versions, setVersions] = useState([]); - const [isLoadedMoreVisible, setIsLoadedMoreVisible] = useState(true); - const referenceData = useContext(DataContext); - - // display more cards if loaded more - // useEffect(() => { - // if (data?.length) { - // setVersions(prevState => [...prevState, ...data]); - // } - // }, [data]); - - // useEffect(() => { - // setIsLoadedMoreVisible(Boolean(versions.length !== 0 && versions.length < totalRecords)); - // }, [versions, totalRecords]); - // useEffect(() => () => setVersions([]), []); + const { + actionsMap, + isLoadedMoreVisible, + versionsToDisplay, + } = useVersionHistory(data, totalRecords); const fieldLabelsMap = { administrativeNotes: formatMessage({ id: 'ui-inventory.administrativeNotes' }), @@ -107,16 +106,20 @@ const InstanceVersionHistory = ({ uri: value => value || , }; + const handleLoadMore = lastEventTs => { + setLastVersionEventTs(lastEventTs); + }; return ( ); }; diff --git a/src/hooks/index.js b/src/hooks/index.js index 1edd7412c..8dae0cf7f 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -15,6 +15,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 useVersionHistory } from './useVersionHistory'; export * from './useQuickExport'; export * from '@folio/stripes-inventory-components/lib/queries/useInstanceDateTypes'; export * from './useCallNumberTypesQuery'; 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;