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

View File

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

View File

@ -7,6 +7,7 @@ import {
cleanQuery,
getGenericActionName,
getKey,
organizeItemsById,
getNamespaceKeys,
getRequestIdentifier,
getRestPath,
@ -15,6 +16,7 @@ import {
isValidIdQuery,
parseId,
} from '../utils';
import type { Item } from '../types';
describe( 'utils', () => {
it( 'should get the rest path when no parameters are given', () => {
@ -247,4 +249,89 @@ describe( 'utils', () => {
const sanitizedArgs = maybeReplaceIdQuery( args, '/my/namespace/' );
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
*/
import CRUD_ACTIONS from './crud-actions';
import { IdQuery, IdType, ItemQuery } from './types';
import { getResourceName } from '../utils';
import type { IdQuery, IdType, Item, ItemQuery } from './types';
/**
* 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;
};
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.
*