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:
parent
48ebe7b84c
commit
65d71fa1e2
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Data: reduxify suggested products
|
|
@ -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;
|
||||
|
|
|
@ -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< {
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue