Data: reduxify suggested products (#44043)

* use Thunks in wc/admin/products store

* first implementation

* changelog

* fixing tests

* remove unused __meta prop

* update constant name

* fix eslint issue
This commit is contained in:
Damián Suárez 2024-01-24 17:56:41 -03:00 committed by GitHub
parent 48ebe7b84c
commit 65d71fa1e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 129 additions and 7 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Data: reduxify suggested products

View File

@ -14,6 +14,7 @@ export enum TYPES {
DELETE_PRODUCT_START = 'DELETE_PRODUCT_START',
DELETE_PRODUCT_ERROR = 'DELETE_PRODUCT_ERROR',
DELETE_PRODUCT_SUCCESS = 'DELETE_PRODUCT_SUCCESS',
SET_SUGGESTED_PRODUCTS = 'SET_SUGGESTED_PRODUCTS',
}
export default TYPES;

View File

@ -216,6 +216,14 @@ export function* deleteProduct(
}
}
export function setSuggestedProductAction( key: string, items: Product[] ) {
return {
type: TYPES.SET_SUGGESTED_PRODUCTS as const,
key,
items,
};
}
export type Actions = ReturnType<
| typeof createProductStart
| typeof createProductError
@ -232,6 +240,7 @@ export type Actions = ReturnType<
| typeof deleteProductStart
| typeof deleteProductSuccess
| typeof deleteProductError
| typeof setSuggestedProductAction
>;
export type ActionDispatchers = DispatchFromMap< {

View File

@ -2,3 +2,4 @@ export const STORE_NAME = 'wc/admin/products';
export const WC_PRODUCT_NAMESPACE = '/wc/v3/products';
export const PERMALINK_PRODUCT_REGEX = /%(?:postname|pagename)%/;
export const WC_V3_ENDPOINT_SUGGESTED_PRODUCTS = `${ WC_PRODUCT_NAMESPACE }/suggested-products`;

View File

@ -16,6 +16,8 @@ import reducer, { ProductState, State } from './reducer';
import controls from '../controls';
registerStore< State >( STORE_NAME, {
// @ts-expect-error There are no types for this.
__experimentalUseThunks: true,
reducer: reducer as Reducer< ProductState >,
actions,
controls,

View File

@ -8,7 +8,11 @@ import { Reducer } from 'redux';
*/
import TYPES from './action-types';
import { Actions } from './actions';
import { PartialProduct } from './types';
import type {
PartialProduct,
Product,
SuggestedProductOptionsKey,
} from './types';
import {
getProductResourceName,
getTotalProductCountResourceName,
@ -29,6 +33,12 @@ export type ProductState = {
updateProduct?: Record< number, boolean >;
deleteProduct?: Record< number, boolean >;
};
suggestedProducts: {
[ key in SuggestedProductOptionsKey ]: {
items: Product[];
};
};
};
const reducer: Reducer< ProductState, Actions > = (
@ -38,6 +48,7 @@ const reducer: Reducer< ProductState, Actions > = (
errors: {},
data: {},
pending: {},
suggestedProducts: {},
},
payload
) => {
@ -190,6 +201,17 @@ const reducer: Reducer< ProductState, Actions > = (
},
},
};
case TYPES.SET_SUGGESTED_PRODUCTS: {
return {
...state,
suggestedProducts: {
...state.suggestedProducts,
[ payload.key ]: {
items: payload.items || [],
},
},
};
}
default:
return state;
}

View File

@ -2,8 +2,10 @@
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import apiFetch from '@wordpress/api-fetch';
import {
apiFetch,
apiFetch as controlsApiFetch,
dispatch as deprecatedDispatch,
select,
} from '@wordpress/data-controls';
@ -12,8 +14,12 @@ import { controls } from '@wordpress/data';
/**
* Internal dependencies
*/
import { STORE_NAME, WC_PRODUCT_NAMESPACE } from './constants';
import { Product, ProductQuery } from './types';
import {
STORE_NAME,
WC_PRODUCT_NAMESPACE,
WC_V3_ENDPOINT_SUGGESTED_PRODUCTS,
} from './constants';
import { GetSuggestedProductsOptions, Product, ProductQuery } from './types';
import {
getProductError,
getProductsError,
@ -23,6 +29,7 @@ import {
getProductSuccess,
} from './actions';
import { request } from '../utils';
import { createIdFromOptions } from './utils';
const dispatch =
controls && controls.dispatch ? controls.dispatch : deprecatedDispatch;
@ -58,7 +65,7 @@ export function* getProducts( query: Partial< ProductQuery > ) {
export function* getProduct( productId: number ) {
try {
const product: Product = yield apiFetch( {
const product: Product = yield controlsApiFetch( {
path: addQueryArgs( `${ WC_PRODUCT_NAMESPACE }/${ productId }`, {
context: 'edit',
} ),
@ -130,3 +137,16 @@ export function* getProductsTotalCount( query: Partial< ProductQuery > ) {
export function* getPermalinkParts( productId: number ) {
yield resolveSelect( STORE_NAME, 'getProduct', [ productId ] );
}
export const getSuggestedProducts =
( options: GetSuggestedProductsOptions ) =>
// @ts-expect-error There are no types for this.
async ( { dispatch: contextualDispatch } ) => {
const key = createIdFromOptions( options );
const data = await apiFetch( {
path: addQueryArgs( WC_V3_ENDPOINT_SUGGESTED_PRODUCTS, options ),
} );
contextualDispatch.setSuggestedProductAction( key, data );
};

View File

@ -7,12 +7,17 @@ import createSelector from 'rememo';
* Internal dependencies
*/
import {
createIdFromOptions,
getProductResourceName,
getTotalProductCountResourceName,
} from './utils';
import { WPDataSelector, WPDataSelectors } from '../types';
import { ProductState } from './reducer';
import { PartialProduct, ProductQuery } from './types';
import {
GetSuggestedProductsOptions,
PartialProduct,
ProductQuery,
} from './types';
import { ActionDispatchers } from './actions';
import { PERMALINK_PRODUCT_REGEX } from './constants';
@ -172,6 +177,26 @@ export const getRelatedProducts = createSelector(
}
);
/**
* Return an array of suggested products the
* given options.
*
* @param {ProductState} state - The current state.
* @param {GetSuggestedProductsOptions} options - The options.
* @return {PartialProduct[]} The suggested products.
*/
export function getSuggestedProducts(
state: ProductState,
options: GetSuggestedProductsOptions
): PartialProduct[] {
const key = createIdFromOptions( options );
if ( ! state.suggestedProducts[ key ] ) {
return [];
}
return state.suggestedProducts[ key ].items;
}
export type ProductsSelectors = {
getCreateProductError: WPDataSelector< typeof getCreateProductError >;
getProduct: WPDataSelector< typeof getProduct >;
@ -181,4 +206,5 @@ export type ProductsSelectors = {
isPending: WPDataSelector< typeof isPending >;
getPermalinkParts: WPDataSelector< typeof getPermalinkParts >;
getRelatedProducts: WPDataSelector< typeof getRelatedProducts >;
getSuggestedProducts: WPDataSelector< typeof getSuggestedProducts >;
} & WPDataSelectors;

View File

@ -16,6 +16,7 @@ const defaultState: ProductState = {
errors: {},
data: {},
pending: {},
suggestedProducts: {},
};
describe( 'products reducer', () => {
@ -42,6 +43,7 @@ describe( 'products reducer', () => {
2: { id: 2, name: 'Sauce', status: 'publish' },
},
pending: {},
suggestedProducts: {},
};
const update: PartialProduct = {
id: 2,
@ -252,6 +254,7 @@ describe( 'products reducer', () => {
2: true,
},
},
suggestedProducts: {},
};
const product: PartialProduct = {
id: 2,
@ -322,6 +325,7 @@ describe( 'products reducer', () => {
2: true,
},
},
suggestedProducts: {},
};
const product: PartialProduct = {
id: 1,

View File

@ -195,3 +195,15 @@ export type ProductQuery<
max_price?: string;
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
};
export type SuggestedProductOptionsKey = string;
/*
* Selector types
*/
export type GetSuggestedProductsOptions = {
categories?: number[];
tags?: number[];
attributes?: number[];
limit?: number;
};

View File

@ -2,7 +2,7 @@
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { ProductQuery } from './types';
import type { GetSuggestedProductsOptions, ProductQuery } from './types';
const PRODUCT_PREFIX = 'product';
@ -31,3 +31,24 @@ export function getTotalProductCountResourceName(
const { _fields, page, per_page, ...totalsQuery } = query;
return getProductResourceName( totalsQuery );
}
/**
* Create a unique string ID based the options object.
*
* @param {GetSuggestedProductsOptions} options - Options to create the ID from.
* @return {string} Unique ID.
*/
export function createIdFromOptions(
options: GetSuggestedProductsOptions = {}
): string {
if ( ! Object.keys( options ).length ) {
return 'default';
}
const optionsForKey = { ...options };
options.categories?.sort();
options.tags?.sort();
options.attributes?.sort();
return JSON.stringify( optionsForKey );
}