diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 1969d2cd717a2..714163902b2d7 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -14,6 +14,7 @@ import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ +import { getNestedValue, setNestedValue } from './utils'; import { receiveItems, removeItems, receiveQueriedItems } from './queried-data'; import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities'; import { createBatch } from './batch'; @@ -779,7 +780,7 @@ export const saveEditedEntityRecord = * @param {string} kind Kind of the entity. * @param {string} name Name of the entity. * @param {Object} recordId ID of the record. - * @param {Array} itemsToSave List of entity properties to save. + * @param {Array} itemsToSave List of entity properties or property paths to save. * @param {Object} options Saving options. */ export const __experimentalSaveSpecifiedEntityEdits = @@ -794,10 +795,9 @@ export const __experimentalSaveSpecifiedEntityEdits = recordId ); const editsToSave = {}; - for ( const edit in edits ) { - if ( itemsToSave.some( ( item ) => item === edit ) ) { - editsToSave[ edit ] = edits[ edit ]; - } + + for ( const item of itemsToSave ) { + setNestedValue( editsToSave, item, getNestedValue( edits, item ) ); } const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); @@ -814,7 +814,6 @@ export const __experimentalSaveSpecifiedEntityEdits = if ( recordId ) { editsToSave[ entityIdKey ] = recordId; } - return await dispatch.saveEntityRecord( kind, name, diff --git a/packages/core-data/src/utils/get-nested-value.js b/packages/core-data/src/utils/get-nested-value.js new file mode 100644 index 0000000000000..0fe210159b07b --- /dev/null +++ b/packages/core-data/src/utils/get-nested-value.js @@ -0,0 +1,27 @@ +/** + * Helper util to return a value from a certain path of the object. + * Path is specified as either: + * - a string of properties, separated by dots, for example: "x.y". + * - an array of properties, for example `[ 'x', 'y' ]`. + * You can also specify a default value in case the result is nullish. + * + * @param {Object} object Input object. + * @param {string|Array} path Path to the object property. + * @param {*} defaultValue Default value if the value at the specified path is undefined. + * @return {*} Value of the object property at the specified path. + */ +export default function getNestedValue( object, path, defaultValue ) { + if ( + ! object || + typeof object !== 'object' || + ( typeof path !== 'string' && ! Array.isArray( path ) ) + ) { + return object; + } + const normalizedPath = Array.isArray( path ) ? path : path.split( '.' ); + let value = object; + normalizedPath.forEach( ( fieldName ) => { + value = value?.[ fieldName ]; + } ); + return value !== undefined ? value : defaultValue; +} diff --git a/packages/core-data/src/utils/index.js b/packages/core-data/src/utils/index.js index 4f4149c8265b2..f37efe6eee7fd 100644 --- a/packages/core-data/src/utils/index.js +++ b/packages/core-data/src/utils/index.js @@ -7,3 +7,4 @@ export { default as replaceAction } from './replace-action'; export { default as withWeakMapCache } from './with-weak-map-cache'; export { default as isRawAttribute } from './is-raw-attribute'; export { default as setNestedValue } from './set-nested-value'; +export { default as getNestedValue } from './get-nested-value'; diff --git a/packages/core-data/src/utils/set-nested-value.js b/packages/core-data/src/utils/set-nested-value.js index e90bf23e4dad8..cb2ae788d1c92 100644 --- a/packages/core-data/src/utils/set-nested-value.js +++ b/packages/core-data/src/utils/set-nested-value.js @@ -4,6 +4,10 @@ * Arrays are created for missing index properties while objects are created * for all other missing properties. * + * Path is specified as either: + * - a string of properties, separated by dots, for example: "x.y". + * - an array of properties, for example `[ 'x', 'y' ]`. + * * This function intentionally mutates the input object. * * Inspired by _.set(). @@ -12,24 +16,26 @@ * * @todo Needs to be deduplicated with its copy in `@wordpress/edit-site`. * - * @param {Object} object Object to modify - * @param {Array} path Path of the property to set. - * @param {*} value Value to set. + * @param {Object} object Object to modify + * @param {Array|string} path Path of the property to set. + * @param {*} value Value to set. */ export default function setNestedValue( object, path, value ) { if ( ! object || typeof object !== 'object' ) { return object; } - path.reduce( ( acc, key, idx ) => { + const normalizedPath = Array.isArray( path ) ? path : path.split( '.' ); + + normalizedPath.reduce( ( acc, key, idx ) => { if ( acc[ key ] === undefined ) { - if ( Number.isInteger( path[ idx + 1 ] ) ) { + if ( Number.isInteger( normalizedPath[ idx + 1 ] ) ) { acc[ key ] = []; } else { acc[ key ] = {}; } } - if ( idx === path.length - 1 ) { + if ( idx === normalizedPath.length - 1 ) { acc[ key ] = value; } return acc[ key ]; diff --git a/packages/core-data/src/utils/test/get-nested-value.js b/packages/core-data/src/utils/test/get-nested-value.js new file mode 100644 index 0000000000000..1048885d39c67 --- /dev/null +++ b/packages/core-data/src/utils/test/get-nested-value.js @@ -0,0 +1,61 @@ +/** + * Internal dependencies + */ +import getNestedValue from '../get-nested-value'; + +describe( 'getNestedValue', () => { + it( 'should return the same object unmodified if path is an empty array', () => { + const input = { x: 'y' }; + const result = getNestedValue( input, [] ); + expect( result ).toEqual( input ); + } ); + + it( 'should return the nested value', () => { + const input = { x: { y: { z: 123 } } }; + const result = getNestedValue( input, [ 'x', 'y', 'z' ] ); + + expect( result ).toEqual( 123 ); + } ); + + it( 'should return the nested value if the path is a string', () => { + const input = { x: { y: { z: 123 } } }; + const result = getNestedValue( input, 'x.y.z' ); + + expect( result ).toEqual( 123 ); + } ); + + it( 'should return the shallow value', () => { + const input = { x: { y: { z: 123 } } }; + const result = getNestedValue( input, 'x' ); + + expect( result ).toEqual( { y: { z: 123 } } ); + } ); + + it( 'should return the default value if the nested value is undefined', () => { + const input = { x: { y: { z: undefined } } }; + const result = getNestedValue( input, [ 'x', 'y', 'z' ], 456 ); + + expect( result ).toEqual( 456 ); + } ); + + it( 'should return the nested value if it is different to undefined', () => { + const input = { x: { y: { z: null } } }; + const result = getNestedValue( input, 'x.y.z', 456 ); + + expect( result ).toBeNull(); + } ); + + it( 'should return the default value if the nested value does not exist', () => { + const input = { x: { y: { z: 123 } } }; + const result = getNestedValue( input, [ 'x', 'y', 'z1' ], 456 ); + + expect( result ).toEqual( 456 ); + } ); + + it( 'should return undefined if the nested value does not exist', () => { + const input = { x: { y: { z: 123 } } }; + const result = getNestedValue( input, [ 'x', 'y', 'z1' ] ); + + expect( result ).toBeUndefined(); + } ); +} ); diff --git a/packages/core-data/src/utils/test/set-nested-value.js b/packages/core-data/src/utils/test/set-nested-value.js index bbc7129180764..7785e94830109 100644 --- a/packages/core-data/src/utils/test/set-nested-value.js +++ b/packages/core-data/src/utils/test/set-nested-value.js @@ -19,6 +19,13 @@ describe( 'setNestedValue', () => { expect( result ).toEqual( { x: { y: { z: 456 } } } ); } ); + it( 'should set values at deep level having a string as path', () => { + const input = { x: { y: { z: 123 } } }; + const result = setNestedValue( input, 'x.y.z', 456 ); + + expect( result ).toEqual( { x: { y: { z: 456 } } } ); + } ); + it( 'should create nested objects if necessary', () => { const result = setNestedValue( {}, [ 'x', 'y', 'z' ], 123 );