CRUD: introduce organizeItemsById helper function (#47624)

* introduce processItems() util function

* add processItems fn tests

* use processItems in the reducer process

* udpate GET_ITEMS reducer test

* minor CREATE_ITEM_SUCCESS test update

* changelog
This commit is contained in:
Damián Suárez 2024-05-21 10:15:22 +01:00 committed by GitHub
parent 64b8c680fc
commit f5713b3f94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 143 additions and 29 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
CRUD: introduce organizeItemsById helper function

View File

@ -8,10 +8,10 @@ import { Reducer } from 'redux';
*/ */
import { Actions } from './actions'; import { Actions } from './actions';
import CRUD_ACTIONS from './crud-actions'; import CRUD_ACTIONS from './crud-actions';
import { getKey, getRequestIdentifier } from './utils'; import { getRequestIdentifier, organizeItemsById } from './utils';
import { getTotalCountResourceName } from '../utils'; import { getTotalCountResourceName } from '../utils';
import { IdType, Item, ItemQuery } from './types';
import { TYPES } from './action-types'; import { TYPES } from './action-types';
import type { IdType, Item, ItemQuery } from './types';
export type Data = Record< IdType, Item >; export type Data = Record< IdType, Item >;
export type ResourceState = { export type ResourceState = {
@ -291,19 +291,11 @@ export const createReducer = (
}; };
case TYPES.GET_ITEMS_SUCCESS: case TYPES.GET_ITEMS_SUCCESS:
const ids: IdType[] = []; const { objItems, ids } = organizeItemsById(
payload.items,
const nextResources = payload.items.reduce< payload.urlParameters,
Record< string, Item > itemData
>( ( result, item ) => { );
const key = getKey( item.id, payload.urlParameters );
ids.push( key );
result[ key ] = {
...( state.data[ key ] || {} ),
...item,
};
return result;
}, {} );
const itemQuery = getRequestIdentifier( const itemQuery = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS, CRUD_ACTIONS.GET_ITEMS,
@ -318,7 +310,7 @@ export const createReducer = (
}, },
data: { data: {
...state.data, ...state.data,
...nextResources, ...objItems,
}, },
}; };

View File

@ -111,7 +111,7 @@ describe( 'crud reducer', () => {
type: TYPES.GET_ITEMS_SUCCESS, type: TYPES.GET_ITEMS_SUCCESS,
items, items,
query, query,
urlParameters: [ 5 ], urlParameters: [ 100, 5 ],
} ); } );
const resourceName = getRequestIdentifier( const resourceName = getRequestIdentifier(
@ -120,8 +120,8 @@ describe( 'crud reducer', () => {
); );
expect( state.items[ resourceName ].data ).toHaveLength( 2 ); expect( state.items[ resourceName ].data ).toHaveLength( 2 );
expect( state.data[ '5/1' ] ).toEqual( items[ 0 ] ); expect( state.data[ '100/5/1' ] ).toEqual( items[ 0 ] );
expect( state.data[ '5/2' ] ).toEqual( items[ 1 ] ); expect( state.data[ '100/5/2' ] ).toEqual( items[ 1 ] );
} ); } );
it( 'GET_ITEMS_SUCCESS should not remove previously added fields, only update new ones', () => { it( 'GET_ITEMS_SUCCESS should not remove previously added fields, only update new ones', () => {
@ -343,10 +343,7 @@ describe( 'crud reducer', () => {
expect( state.data[ 2 ].status ).toEqual( item.status ); expect( state.data[ 2 ].status ).toEqual( item.status );
expect( state.requesting[ resourceName ] ).toEqual( false ); expect( state.requesting[ resourceName ] ).toEqual( false );
// Test the items object is an empty object
expect( state.items ).toEqual( {} ); expect( state.items ).toEqual( {} );
// Test the itemsCount object is an empty object
expect( state.itemsCount ).toEqual( {} ); expect( state.itemsCount ).toEqual( {} );
} ); } );
@ -383,32 +380,27 @@ describe( 'crud reducer', () => {
); );
expect( state.requesting[ resourceName ] ).toEqual( false ); expect( state.requesting[ resourceName ] ).toEqual( false );
// Items
const itemQuery = getRequestIdentifier( const itemQuery = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS, CRUD_ACTIONS.GET_ITEMS,
options.optimisticQueryUpdate options.optimisticQueryUpdate
); );
// Items should have the item id
expect( state.items[ itemQuery ].data ).toHaveLength( 1 ); expect( state.items[ itemQuery ].data ).toHaveLength( 1 );
expect( state.items[ itemQuery ].data[ 0 ] ).toEqual( 7 ); // Item id expect( state.items[ itemQuery ].data[ 0 ] ).toEqual( 7 ); // Item id
// Items Key
const itemsKey = Object.keys( state.items ); const itemsKey = Object.keys( state.items );
expect( itemsKey ).toHaveLength( 1 ); expect( itemsKey ).toHaveLength( 1 );
expect( itemsKey[ 0 ] ).toEqual( itemQuery ); expect( itemsKey[ 0 ] ).toEqual( itemQuery );
// ItemsCount
const itemsCountQuery = getRequestIdentifier( const itemsCountQuery = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS, CRUD_ACTIONS.GET_ITEMS,
options.optimisticQueryUpdate options.optimisticQueryUpdate
); );
// ItemsCount Key
const itemsCountKey = Object.keys( state.itemsCount ); const itemsCountKey = Object.keys( state.itemsCount );
// ItemsCount should be 1 // ItemsCount should be 1
expect( state.itemsCount[ itemsCountQuery ] ).toEqual( 1 ); // Items count expect( state.itemsCount[ itemsCountQuery ] ).toEqual( 1 );
expect( itemsCountKey ).toHaveLength( 1 ); expect( itemsCountKey ).toHaveLength( 1 );
expect( itemsCountKey[ 0 ] ).toEqual( itemsCountQuery ); expect( itemsCountKey[ 0 ] ).toEqual( itemsCountQuery );

View File

@ -7,6 +7,7 @@ import {
cleanQuery, cleanQuery,
getGenericActionName, getGenericActionName,
getKey, getKey,
organizeItemsById,
getNamespaceKeys, getNamespaceKeys,
getRequestIdentifier, getRequestIdentifier,
getRestPath, getRestPath,
@ -15,6 +16,7 @@ import {
isValidIdQuery, isValidIdQuery,
parseId, parseId,
} from '../utils'; } from '../utils';
import type { Item } from '../types';
describe( 'utils', () => { describe( 'utils', () => {
it( 'should get the rest path when no parameters are given', () => { it( 'should get the rest path when no parameters are given', () => {
@ -247,4 +249,89 @@ describe( 'utils', () => {
const sanitizedArgs = maybeReplaceIdQuery( args, '/my/namespace/' ); const sanitizedArgs = maybeReplaceIdQuery( args, '/my/namespace/' );
expect( sanitizedArgs ).toEqual( args ); expect( sanitizedArgs ).toEqual( args );
} ); } );
describe( 'organizeItemsById', () => {
it( 'should return the object Items and IDs when no urlParameters and existing data is provided', () => {
const items: Item[] = [
{ id: 1, name: 'Yum!' },
{ id: 2, name: 'Dynamite!' },
];
const { objItems, ids } = organizeItemsById( items );
expect( objItems ).toEqual( {
1: { id: 1, name: 'Yum!' },
2: { id: 2, name: 'Dynamite!' },
} );
expect( ids ).toEqual( [ 1, 2 ] );
} );
it( 'should return the object Items and IDs when urlParameters but no existing data is provided', () => {
const items: Item[] = [
{ id: 1, name: 'Yum!' },
{ id: 2, name: 'Dynamite!' },
];
const urlParameters = [ 200, 10 ];
const { objItems, ids } = organizeItemsById( items, urlParameters );
expect( objItems ).toEqual( {
'200/10/1': { id: 1, name: 'Yum!' },
'200/10/2': { id: 2, name: 'Dynamite!' },
} );
expect( ids ).toEqual( [ '200/10/1', '200/10/2' ] );
} );
it( 'should return the object Items and IDs when existing data but no urlParameters is provided', () => {
const items: Item[] = [
{ id: 1, name: 'Yum! Yum!!' },
{ id: 2, name: 'Dynamite!' },
];
const existingData = {
1: { id: 1, name: 'Yum!', price: 8.5 },
2: { id: 2, name: 'Dynamite!', price: 2.5 },
};
const { objItems, ids } = organizeItemsById(
items,
[],
existingData
);
expect( objItems ).toEqual( {
1: { id: 1, name: 'Yum! Yum!!', price: 8.5 },
2: { id: 2, name: 'Dynamite!', price: 2.5 },
} );
expect( ids ).toEqual( [ 1, 2 ] );
} );
it( 'should return the object Items and IDs when urlParameters and existing data is provided', () => {
const items: Item[] = [
{ id: 1, name: 'Yum! Yum!!' },
{ id: 2, name: 'Dynamite!' },
];
const urlParameters = [ 200, 10 ];
const existingData = {
'200/10/1': { id: 1, name: 'Yum!', price: 8.5 },
'200/10/2': { id: 2, name: 'Dynamite!', price: 2.5 },
};
const { objItems, ids } = organizeItemsById(
items,
urlParameters,
existingData
);
expect( objItems ).toEqual( {
'200/10/1': { id: 1, name: 'Yum! Yum!!', price: 8.5 },
'200/10/2': { id: 2, name: 'Dynamite!', price: 2.5 },
} );
expect( ids ).toEqual( [ '200/10/1', '200/10/2' ] );
} );
} );
} ); } );

View File

@ -7,8 +7,8 @@ import { addQueryArgs } from '@wordpress/url';
* Internal dependencies * Internal dependencies
*/ */
import CRUD_ACTIONS from './crud-actions'; import CRUD_ACTIONS from './crud-actions';
import { IdQuery, IdType, ItemQuery } from './types';
import { getResourceName } from '../utils'; import { getResourceName } from '../utils';
import type { IdQuery, IdType, Item, ItemQuery } from './types';
/** /**
* Get a REST path given a template path and URL params. * Get a REST path given a template path and URL params.
@ -57,6 +57,45 @@ export const getKey = ( query: IdQuery, urlParameters: IdType[] = [] ) => {
return urlParameters.join( '/' ) + '/' + id; return urlParameters.join( '/' ) + '/' + id;
}; };
type organizeItemsByIdReturn = {
objItems: Record< string, Item >;
ids: IdType[];
};
/**
* This function takes an array of items and reduces it into a single object,
* where each key is a unique identifier generated by
* combining the item ID and optional URL parameters.
* It also returns an array of these keys (`ids`).
*
* @param {Array<Item>} items - The items to process.
* @param {Array<IdType>} urlParameters - The URL parameters used to generate keys.
* @param {Record<string, Item>} currentState - The current state data to merge with.
* @return {organizeItemsByIdReturn} An object with two properties: `objItems` and `ids`.
*/
export const organizeItemsById = (
items: Item[],
urlParameters: IdType[] = [],
currentState: Record< string, Item > = {}
): organizeItemsByIdReturn => {
const ids: IdType[] = [];
const objItems: Record< string, Item > = {};
const hasUrlParams = urlParameters.length > 0;
items.forEach( ( item ) => {
const key = hasUrlParams ? getKey( item.id, urlParameters ) : item.id;
ids.push( key );
objItems[ key ] = {
...( currentState[ key ] || {} ),
...item,
};
} );
return { objItems, ids };
};
/** /**
* Parse an ID query into a ID string. * Parse an ID query into a ID string.
* *