From 6366d2c6bd5bf8ba1f7ea42f8c2e4b42dc69ba3b Mon Sep 17 00:00:00 2001 From: Yury Saukou Date: Wed, 15 Feb 2023 17:22:14 +0400 Subject: [PATCH] UISACQCOMP-143 Add the ability to get a list of all nested field names when comparing versions (#675) * UISACQCOMP-143 Add the ability to get a list of all nested field names when comparing versions * UISACQCOMP-143 update tests * UISACQCOMP-143 resolve cycle dependency issue * UISACQCOMP-143 add unit test * UISACQCOMP-143 update unit test --- CHANGELOG.md | 1 + .../VersionHistoryPane.test.js | 3 -- .../VersionViewContext/VersionViewContext.js | 2 +- .../getVersionWrappedFormatter.js | 9 +--- .../getVersionWrappedFormatter.test.js | 6 ++- .../useVersionWrappedFormatter.js | 2 +- .../useVersionsDifference.js | 50 +++++++++++++++---- .../useVersionsDifference.test.js | 34 +++++++++++++ lib/utils/objectDifference/index.js | 7 ++- .../objectDifference/objectDifference.js | 3 +- 10 files changed, 90 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 200df6bb..3b665ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * *BREAKING*: Upgrade `react-redux` to `v8`. Refs UISACQCOMP-137. * Do not display version history cards without changes. Refs UISACQCOMP-139. * *BREAKING*: Update `@folio/stripes` to `8.0.0`. Refs UISACQCOMP-140. +* Add the ability to get a list of all nested field names when comparing versions. Refs UISACQCOMP-143. ## [3.3.2](https://github.com/folio-org/stripes-acq-components/tree/v3.3.2) (2022-11-25) [Full Changelog](https://github.com/folio-org/stripes-acq-components/compare/v3.3.1...v3.3.2) diff --git a/lib/VersionHistory/VersionHistoryPane/VersionHistoryPane.test.js b/lib/VersionHistory/VersionHistoryPane/VersionHistoryPane.test.js index 0fca9c45..e371eea0 100644 --- a/lib/VersionHistory/VersionHistoryPane/VersionHistoryPane.test.js +++ b/lib/VersionHistory/VersionHistoryPane/VersionHistoryPane.test.js @@ -15,8 +15,6 @@ jest.mock('../../hooks', () => ({ const TEST_ID = 'testId'; const poLineLabelsMap = { - 'fundDistribution': 'ui-orders.line.accordion.fund', - 'fundDistribution[\\d]': 'ui-orders.line.accordion.fund', 'fundDistribution[\\d].fundId': 'stripes-acq-components.fundDistribution.name', 'fundDistribution[\\d].code': 'stripes-acq-components.fundDistribution.name', 'fundDistribution[\\d].expenseClassId': 'stripes-acq-components.fundDistribution.expenseClass', @@ -97,7 +95,6 @@ describe('VersionHistoryPane', () => { // Changed fields expect(screen.getByText('stripes-acq-components.versionHistory.card.changed')).toBeInTheDocument(); expect(screen.getByText('stripes-acq-components.fundDistribution.value')).toBeInTheDocument(); - expect(screen.getByText('ui-orders.line.accordion.fund')).toBeInTheDocument(); }); it('should not display version cards without changed fields', () => { diff --git a/lib/VersionHistory/VersionViewContext/VersionViewContext.js b/lib/VersionHistory/VersionViewContext/VersionViewContext.js index bb14dd30..4f79f58d 100644 --- a/lib/VersionHistory/VersionViewContext/VersionViewContext.js +++ b/lib/VersionHistory/VersionViewContext/VersionViewContext.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import { createContext, useMemo } from 'react'; import { get } from 'lodash'; -import { useVersionsDifference } from '../hooks'; +import { useVersionsDifference } from '../hooks/useVersionsDifference'; export const VersionViewContext = createContext(); diff --git a/lib/VersionHistory/getVersionWrappedFormatter.js b/lib/VersionHistory/getVersionWrappedFormatter.js index 6b6e4b6d..bc754a92 100644 --- a/lib/VersionHistory/getVersionWrappedFormatter.js +++ b/lib/VersionHistory/getVersionWrappedFormatter.js @@ -1,21 +1,16 @@ -import { getHighlightedFields } from './getHighlightedFields'; - export const getVersionWrappedFormatter = ({ baseFormatter, - changes, fieldsMapping, name, + paths, }) => { const formatterEntries = Object.entries(baseFormatter); - const fieldNames = Object.values(fieldsMapping); - - const highlights = getHighlightedFields({ changes, fieldNames, name }); return formatterEntries.reduce((acc, [colName, renderCell]) => { return { ...acc, [colName]: ({ rowIndex, ...rest }) => { - const isUpdated = highlights.includes(`${name}[${rowIndex}].${fieldsMapping[colName]}`); + const isUpdated = paths?.includes(`${name}[${rowIndex}].${fieldsMapping[colName]}`); const content = renderCell({ rowIndex, ...rest }); diff --git a/lib/VersionHistory/getVersionWrappedFormatter.test.js b/lib/VersionHistory/getVersionWrappedFormatter.test.js index 24622955..4ea01980 100644 --- a/lib/VersionHistory/getVersionWrappedFormatter.test.js +++ b/lib/VersionHistory/getVersionWrappedFormatter.test.js @@ -7,10 +7,12 @@ import { getVersionWrappedFormatter } from './getVersionWrappedFormatter'; const changes = [ { type: FIELD_CHANGE_TYPES.update, path: 'fieldOne[0].name' }, - { type: FIELD_CHANGE_TYPES.create, path: 'fieldOne[1]' }, + { type: FIELD_CHANGE_TYPES.create, path: 'fieldOne[1].name' }, { type: FIELD_CHANGE_TYPES.update, path: 'field.two[0].foo' }, ]; +const paths = changes.map(({ path }) => path); + const COLUMNS = { foo: 'foo' }; const baseFormatter = { [COLUMNS.foo]: jest.fn(({ name }) => name) }; const columnMapping = { [COLUMNS.foo]: 'Column head text' }; @@ -37,7 +39,7 @@ describe('getVersionWrappedFormatter', () => { const name = 'fieldOne'; const formatter = getVersionWrappedFormatter({ baseFormatter, - changes, + paths, fieldsMapping, name, }); diff --git a/lib/VersionHistory/hooks/useVersionWrappedFormatter/useVersionWrappedFormatter.js b/lib/VersionHistory/hooks/useVersionWrappedFormatter/useVersionWrappedFormatter.js index d7ee7c8c..ff02888a 100644 --- a/lib/VersionHistory/hooks/useVersionWrappedFormatter/useVersionWrappedFormatter.js +++ b/lib/VersionHistory/hooks/useVersionWrappedFormatter/useVersionWrappedFormatter.js @@ -13,7 +13,7 @@ export const useVersionWrappedFormatter = ({ baseFormatter, name, fieldsMapping baseFormatter, fieldsMapping, name, - changes: versionContext.changes, + paths: versionContext.paths, }); }, [baseFormatter, fieldsMapping, name, versionContext]); diff --git a/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.js b/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.js index aeaa583d..72f82d0a 100644 --- a/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.js +++ b/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.js @@ -1,7 +1,29 @@ -import { get, uniqBy } from 'lodash'; +import { get, isObject, uniqBy } from 'lodash'; import { useMemo } from 'react'; -import { objectDifference } from '../../../utils'; +import { + FIELD_CHANGE_TYPES, + finalFormPathBuilder, + getObjectKey, + objectDifference, +} from '../../../utils'; + +const getNestedObjectKeys = (path, value) => { + if (!isObject(value)) return [path]; + + return Object + .entries(value) + .flatMap(([key, val]) => getNestedObjectKeys( + finalFormPathBuilder([path, getObjectKey(val, key)]), + val, + )); +}; + +const getNestedFieldPaths = (type, path, [oldValue, newValue]) => { + const targetValue = type === FIELD_CHANGE_TYPES.delete ? oldValue : newValue; + + return getNestedObjectKeys(path, targetValue); +}; export const useVersionsDifference = (auditEvents, snapshotPath) => { const users = useMemo(() => uniqBy(auditEvents.map(({ userId, username }) => ({ id: userId, username })), 'id'), [auditEvents]); @@ -11,15 +33,21 @@ export const useVersionsDifference = (auditEvents, snapshotPath) => { acc[event.id] = (i === (auditEvents.length - 1)) ? null - : objectDifference(get(auditEvents[i + 1], snapshotPath, {}), snapshot).reduce((accum, item) => { - accum.changes.push(item); - accum.paths.push(item.path); - - return accum; - }, { - changes: [], - paths: [], - }); + : objectDifference(get(auditEvents[i + 1], snapshotPath, {}), snapshot) + .reduce((accum, item) => { + const { path, type, values } = item; + const paths = type === FIELD_CHANGE_TYPES.update + ? [path] + : getNestedFieldPaths(type, path, values); + + accum.changes.push(item); + accum.paths.push(...paths); + + return accum; + }, { + changes: [], + paths: [], + }); return acc; }, {}), [auditEvents, snapshotPath]); diff --git a/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.test.js b/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.test.js index 96a0e4cd..c619d537 100644 --- a/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.test.js +++ b/lib/VersionHistory/hooks/useVersionsDifference/useVersionsDifference.test.js @@ -30,4 +30,38 @@ describe('useVersionsDifference', () => { }, }); }); + + it('should return paths for all created or deleted fields with nesting', () => { + const event = { + id: 'event-id', + username: 'testuser', + snapshot: { + id: 'test-id', + notNestingField: 'Hello', + fieldWithNesting: { + first: 'first field', + second: 'second field', + }, + }, + }; + + const clonedEvent = Object.assign(cloneDeep(event), { id: 'clonedEventId', username: 'testuser' }); + + set(clonedEvent.snapshot, 'fanotherFeldWithNesting', { foo: 'bar' }); + set(clonedEvent.snapshot, 'notNestingField', 'value'); + unset(clonedEvent.snapshot, 'fieldWithNesting'); + + const { result } = renderHook(() => useVersionsDifference([clonedEvent, event], 'snapshot')); + + const { versionsMap } = result.current; + + expect(versionsMap[clonedEvent.id]).toEqual(expect.objectContaining({ + paths: expect.arrayContaining([ + 'notNestingField', + 'fanotherFeldWithNesting.foo', // added in the object + 'fieldWithNesting.first', // removed from the object + 'fieldWithNesting.second', // removed from the object + ]), + })); + }); }); diff --git a/lib/utils/objectDifference/index.js b/lib/utils/objectDifference/index.js index b8e1d596..24da396f 100644 --- a/lib/utils/objectDifference/index.js +++ b/lib/utils/objectDifference/index.js @@ -1 +1,6 @@ -export { FIELD_CHANGE_TYPES, objectDifference } from './objectDifference'; +export { + FIELD_CHANGE_TYPES, + finalFormPathBuilder, + getObjectKey, + objectDifference, +} from './objectDifference'; diff --git a/lib/utils/objectDifference/objectDifference.js b/lib/utils/objectDifference/objectDifference.js index 2da5d87e..31a20c41 100644 --- a/lib/utils/objectDifference/objectDifference.js +++ b/lib/utils/objectDifference/objectDifference.js @@ -6,7 +6,8 @@ import { } from 'lodash'; const isEmptyObject = (value) => isObject(value) && isEmpty(value); -const getObjectKey = (obj, key) => ( + +export const getObjectKey = (obj, key) => ( Array.isArray(obj) && Number.isInteger(+key) ? Number(key) : key