Add types to hooks (https://github.com/woocommerce/woocommerce-blocks/pull/3913)
* type hooks * type useStoreCart * type the rest * Remove old typedefs * Specify that the return value from useDispatch is a Promise< void > * Change return type of removeItem * If ref.current is undefined then return value without checking shallowEq * Revert "Specify that the return value from useDispatch is a Promise< void >" This reverts commit 97863bd584d38024398913a79ce63fa6b964846a. * Remove type parameter from removeItem and changeCartItemQuantity * Change action return type to cater for generator functions * Remove type parameter from addItemToCart * Add mapped types file to help with actions that are generator functions * Include addItemToCart in return types of cart actions * Use custom DispatchFromMap * Add todos for why we've redefined functionality that exists in Calypso * update types * remove fromEntries and use polyfill * address review * ignore ts no shadow * fix test errors Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
This commit is contained in:
parent
1a010d70fa
commit
dff4772d82
|
@ -85,5 +85,13 @@ module.exports = {
|
|||
'@typescript-eslint/no-shadow': [ 'error' ],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [ './assets/js/mapped-types.ts' ],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'no-shadow': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -16,15 +16,13 @@ describe( 'assertValidContextValue', () => {
|
|||
},
|
||||
};
|
||||
it.each`
|
||||
testValue | expectedMessage | expectError
|
||||
${ {} } | ${ 'expected' } | ${ true }
|
||||
${ 10 } | ${ 'expected' } | ${ true }
|
||||
${ { amountKetchup: 20 } } | ${ 'not expected' } | ${ false }
|
||||
${ { amountKetchup: '10' } } | ${ 'expected' } | ${ true }
|
||||
${ { cheeseburger: 'fries', amountKetchup: 20 } } | ${ 'not expected' } | ${ false }
|
||||
testValue
|
||||
${ {} }
|
||||
${ 10 }
|
||||
${ { amountKetchup: '10' } }
|
||||
`(
|
||||
'The value of $testValue is $expectedMessage to trigger an Error',
|
||||
( { testValue, expectError } ) => {
|
||||
'The value of $testValue is expected to trigger an Error',
|
||||
( { testValue } ) => {
|
||||
const invokeTest = () => {
|
||||
assertValidContextValue(
|
||||
contextName,
|
||||
|
@ -32,11 +30,24 @@ describe( 'assertValidContextValue', () => {
|
|||
testValue
|
||||
);
|
||||
};
|
||||
if ( expectError ) {
|
||||
expect( invokeTest ).toThrow();
|
||||
} else {
|
||||
expect( invokeTest ).not.toThrow();
|
||||
}
|
||||
expect( invokeTest ).toThrow();
|
||||
}
|
||||
);
|
||||
it.each`
|
||||
testValue
|
||||
${ { amountKetchup: 20 } }
|
||||
${ { cheeseburger: 'fries', amountKetchup: 20 } }
|
||||
`(
|
||||
'The value of $testValue is not expected to trigger an Error',
|
||||
( { testValue } ) => {
|
||||
const invokeTest = () => {
|
||||
assertValidContextValue(
|
||||
contextName,
|
||||
validationMap,
|
||||
testValue
|
||||
);
|
||||
};
|
||||
expect( invokeTest ).not.toThrow();
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useSelect } from '@wordpress/data';
|
|||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
import type { StoreCartCoupon } from '@woocommerce/types';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -23,12 +23,15 @@ import { useStoreNotices } from '../use-store-notices';
|
|||
* @return {StoreCartCoupon} An object exposing data and actions from/for the
|
||||
* store api /cart/coupons endpoint.
|
||||
*/
|
||||
export const useStoreCartCoupons = () => {
|
||||
export const useStoreCartCoupons = (): StoreCartCoupon => {
|
||||
const { cartCoupons, cartIsLoading } = useStoreCart();
|
||||
const { addErrorNotice, addSnackbarNotice } = useStoreNotices();
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
|
||||
const results = useSelect(
|
||||
const results: Pick<
|
||||
StoreCartCoupon,
|
||||
'applyCoupon' | 'removeCoupon' | 'isApplyingCoupon' | 'isRemovingCoupon'
|
||||
> = useSelect(
|
||||
( select, { dispatch } ) => {
|
||||
const store = select( storeKey );
|
||||
const isApplyingCoupon = store.isApplyingCoupon();
|
||||
|
@ -37,9 +40,13 @@ export const useStoreCartCoupons = () => {
|
|||
applyCoupon,
|
||||
removeCoupon,
|
||||
receiveApplyingCoupon,
|
||||
}: {
|
||||
applyCoupon: ( coupon: string ) => Promise< boolean >;
|
||||
removeCoupon: ( coupon: string ) => Promise< boolean >;
|
||||
receiveApplyingCoupon: ( coupon: string ) => void;
|
||||
} = dispatch( storeKey );
|
||||
|
||||
const applyCouponWithNotices = ( couponCode ) => {
|
||||
const applyCouponWithNotices = ( couponCode: string ) => {
|
||||
applyCoupon( couponCode )
|
||||
.then( ( result ) => {
|
||||
if ( result === true ) {
|
||||
|
@ -70,7 +77,7 @@ export const useStoreCartCoupons = () => {
|
|||
} );
|
||||
};
|
||||
|
||||
const removeCouponWithNotices = ( couponCode ) => {
|
||||
const removeCouponWithNotices = ( couponCode: string ) => {
|
||||
removeCoupon( couponCode )
|
||||
.then( ( result ) => {
|
||||
if ( result === true ) {
|
|
@ -2,23 +2,17 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { useState, useEffect, useCallback } from '@wordpress/element';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||
import { triggerFragmentRefresh } from '@woocommerce/base-utils';
|
||||
|
||||
import type { CartItem, StoreCartItemQuantity } from '@woocommerce/types';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreCart } from './use-store-cart';
|
||||
import { usePrevious } from '../use-previous';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/hooks').StoreCartItemQuantity} StoreCartItemQuantity
|
||||
* @typedef {import('@woocommerce/type-defs/cart').CartItem} CartItem
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a custom hook for loading the Store API /cart/ endpoint and
|
||||
* actions for removing or changing item quantity.
|
||||
|
@ -30,23 +24,17 @@ import { usePrevious } from '../use-previous';
|
|||
* @return {StoreCartItemQuantity} An object exposing data and actions relating
|
||||
* to cart items.
|
||||
*/
|
||||
export const useStoreCartItemQuantity = ( cartItem ) => {
|
||||
export const useStoreCartItemQuantity = (
|
||||
cartItem: CartItem
|
||||
): StoreCartItemQuantity => {
|
||||
const { key: cartItemKey = '', quantity: cartItemQuantity = 1 } = cartItem;
|
||||
const { cartErrors } = useStoreCart();
|
||||
const { dispatchActions } = useCheckoutContext();
|
||||
|
||||
// Store quantity in hook state. This is used to keep the UI
|
||||
// updated while server request is updated.
|
||||
const [ quantity, changeQuantity ] = useState( cartItemQuantity );
|
||||
// Quantity can be updated from somewhere else, this is to keep it in sync.
|
||||
useEffect( () => {
|
||||
changeQuantity( ( currentQuantity ) =>
|
||||
cartItemQuantity !== currentQuantity
|
||||
? cartItemQuantity
|
||||
: currentQuantity
|
||||
);
|
||||
}, [ cartItemQuantity ] );
|
||||
const [ debouncedQuantity ] = useDebounce( quantity, 400 );
|
||||
const [ quantity, changeQuantity ] = useState< number >( cartItemQuantity );
|
||||
const [ debouncedQuantity ] = useDebounce< number >( quantity, 400 );
|
||||
const previousDebouncedQuantity = usePrevious( debouncedQuantity );
|
||||
const { removeItemFromCart, changeCartItemQuantity } = useDispatch(
|
||||
storeKey
|
||||
|
@ -78,13 +66,13 @@ export const useStoreCartItemQuantity = ( cartItem ) => {
|
|||
);
|
||||
const previousIsPendingDelete = usePrevious( isPendingDelete );
|
||||
|
||||
const removeItem = useCallback( () => {
|
||||
const removeItem = () => {
|
||||
return cartItemKey
|
||||
? removeItemFromCart( cartItemKey ).then( () => {
|
||||
triggerFragmentRefresh();
|
||||
} )
|
||||
: false;
|
||||
}, [ cartItemKey, removeItemFromCart ] );
|
||||
};
|
||||
|
||||
// Observe debounced quantity value, fire action to update server on change.
|
||||
useEffect( () => {
|
|
@ -7,9 +7,20 @@ import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
|||
import { useSelect } from '@wordpress/data';
|
||||
import { useEditorContext } from '@woocommerce/base-context';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { mapValues } from 'lodash';
|
||||
import type {
|
||||
StoreCart,
|
||||
CartResponseTotals,
|
||||
CartResponseFeeItem,
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingAddress,
|
||||
} from '@woocommerce/types';
|
||||
import { fromEntriesPolyfill } from '@woocommerce/base-utils';
|
||||
|
||||
const defaultShippingAddress = {
|
||||
declare module '@wordpress/html-entities' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
export function decodeEntities< T >( coupon: T ): T;
|
||||
}
|
||||
const defaultShippingAddress: CartResponseShippingAddress = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
|
@ -21,20 +32,48 @@ const defaultShippingAddress = {
|
|||
country: '',
|
||||
};
|
||||
|
||||
const defaultBillingAddress = {
|
||||
const defaultBillingAddress: CartResponseBillingAddress = {
|
||||
...defaultShippingAddress,
|
||||
email: '',
|
||||
phone: '',
|
||||
};
|
||||
|
||||
const decodeValues = ( object ) =>
|
||||
mapValues( object, ( value ) => decodeEntities( value ) );
|
||||
const defaultCartTotals: CartResponseTotals = {
|
||||
total_items: '',
|
||||
total_items_tax: '',
|
||||
total_fees: '',
|
||||
total_fees_tax: '',
|
||||
total_discount: '',
|
||||
total_discount_tax: '',
|
||||
total_shipping: '',
|
||||
total_shipping_tax: '',
|
||||
total_price: '',
|
||||
total_tax: '',
|
||||
tax_lines: [],
|
||||
currency_code: '',
|
||||
currency_symbol: '',
|
||||
currency_minor_unit: 2,
|
||||
currency_decimal_separator: '',
|
||||
currency_thousand_separator: '',
|
||||
currency_prefix: '',
|
||||
currency_suffix: '',
|
||||
};
|
||||
|
||||
const decodeValues = (
|
||||
object: Record< string, unknown >
|
||||
): Record< string, unknown > =>
|
||||
fromEntriesPolyfill(
|
||||
Object.entries( object ).map( ( [ key, value ] ) => [
|
||||
key,
|
||||
decodeEntities( value ),
|
||||
] )
|
||||
);
|
||||
|
||||
/**
|
||||
* @constant
|
||||
* @type {StoreCart} Object containing cart data.
|
||||
*/
|
||||
export const defaultCartData = {
|
||||
export const defaultCartData: StoreCart = {
|
||||
cartCoupons: [],
|
||||
cartItems: [],
|
||||
cartFees: [],
|
||||
|
@ -43,7 +82,7 @@ export const defaultCartData = {
|
|||
cartNeedsPayment: true,
|
||||
cartNeedsShipping: true,
|
||||
cartItemErrors: [],
|
||||
cartTotals: {},
|
||||
cartTotals: defaultCartTotals,
|
||||
cartIsLoading: true,
|
||||
cartErrors: [],
|
||||
billingAddress: defaultBillingAddress,
|
||||
|
@ -52,7 +91,7 @@ export const defaultCartData = {
|
|||
shippingRatesLoading: false,
|
||||
cartHasCalculatedShipping: false,
|
||||
paymentRequirements: [],
|
||||
receiveCart: () => {},
|
||||
receiveCart: () => undefined,
|
||||
extensions: {},
|
||||
};
|
||||
|
||||
|
@ -68,12 +107,14 @@ export const defaultCartData = {
|
|||
*
|
||||
* @return {StoreCart} Object containing cart data.
|
||||
*/
|
||||
export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
||||
export const useStoreCart = (
|
||||
options: { shouldSelect: boolean } = { shouldSelect: true }
|
||||
): StoreCart => {
|
||||
const { isEditor, previewData } = useEditorContext();
|
||||
const previewCart = previewData?.previewCart || {};
|
||||
const { shouldSelect } = options;
|
||||
|
||||
const results = useSelect(
|
||||
const results: StoreCart = useSelect(
|
||||
( select, { dispatch } ) => {
|
||||
if ( ! shouldSelect ) {
|
||||
return defaultCartData;
|
||||
|
@ -103,7 +144,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
receiveCart:
|
||||
typeof previewCart?.receiveCart === 'function'
|
||||
? previewCart.receiveCart
|
||||
: () => {},
|
||||
: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -120,7 +161,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
|
|||
const shippingAddress = cartData.needsShipping
|
||||
? decodeValues( cartData.shippingAddress )
|
||||
: billingAddress;
|
||||
const cartFees = cartData.fees.map( ( fee ) =>
|
||||
const cartFees = cartData.fees.map( ( fee: CartResponseFeeItem ) =>
|
||||
decodeValues( fee )
|
||||
);
|
||||
return {
|
|
@ -7,7 +7,15 @@ import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
|||
import { useDebounce } from 'use-debounce';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
import { formatStoreApiErrorMessage } from '@woocommerce/base-utils';
|
||||
import type {
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingAddress,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
declare type CustomerData = {
|
||||
billingData: CartResponseBillingAddress;
|
||||
shippingAddress: CartResponseShippingAddress;
|
||||
};
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -18,7 +26,12 @@ import { useStoreCart } from '../cart';
|
|||
/**
|
||||
* This is a custom hook for syncing customer address data (billing and shipping) with the server.
|
||||
*/
|
||||
export const useCustomerData = () => {
|
||||
export const useCustomerData = (): {
|
||||
billingData: CartResponseBillingAddress;
|
||||
shippingAddress: CartResponseShippingAddress;
|
||||
setBillingData: ( data: CartResponseBillingAddress ) => void;
|
||||
setShippingAddress: ( data: CartResponseBillingAddress ) => void;
|
||||
} => {
|
||||
const { updateCustomerData } = useDispatch( storeKey );
|
||||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
||||
|
||||
|
@ -26,16 +39,18 @@ export const useCustomerData = () => {
|
|||
const {
|
||||
billingAddress: initialBillingAddress,
|
||||
shippingAddress: initialShippingAddress,
|
||||
}: Omit< CustomerData, 'billingData' > & {
|
||||
billingAddress: CartResponseBillingAddress;
|
||||
} = useStoreCart();
|
||||
|
||||
// State of customer data is tracked here from this point, using the initial values from the useStoreCart hook.
|
||||
const [ customerData, setCustomerData ] = useState( {
|
||||
const [ customerData, setCustomerData ] = useState< CustomerData >( {
|
||||
billingData: initialBillingAddress,
|
||||
shippingAddress: initialShippingAddress,
|
||||
} );
|
||||
|
||||
// Store values last sent to the server in a ref to avoid requests unless important fields are changed.
|
||||
const previousCustomerData = useRef( customerData );
|
||||
const previousCustomerData = useRef< CustomerData >( customerData );
|
||||
|
||||
// Debounce updates to the customerData state so it's not triggered excessively.
|
||||
const [ debouncedCustomerData ] = useDebounce( customerData, 1000, {
|
||||
|
@ -114,7 +129,6 @@ export const useCustomerData = () => {
|
|||
removeNotice,
|
||||
updateCustomerData,
|
||||
] );
|
||||
|
||||
return {
|
||||
billingData: customerData.billingData,
|
||||
shippingAddress: customerData.shippingAddress,
|
|
@ -3,6 +3,10 @@
|
|||
*/
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
import { pluckAddress } from '@woocommerce/base-utils';
|
||||
import type {
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingAddress,
|
||||
} from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Does a shallow compare of important address data to determine if the cart needs updating.
|
||||
|
@ -12,7 +16,12 @@ import { pluckAddress } from '@woocommerce/base-utils';
|
|||
*
|
||||
* @return {boolean} True if the store needs updating due to changed data.
|
||||
*/
|
||||
export const shouldUpdateAddressStore = ( previousAddress, address ) => {
|
||||
export const shouldUpdateAddressStore = <
|
||||
T extends CartResponseBillingAddress | CartResponseShippingAddress
|
||||
>(
|
||||
previousAddress: T,
|
||||
address: T
|
||||
): boolean => {
|
||||
if ( ! address.country ) {
|
||||
return false;
|
||||
}
|
|
@ -16,21 +16,15 @@ describe( 'useShallowEqual', () => {
|
|||
let renderer;
|
||||
beforeEach( () => ( renderer = null ) );
|
||||
it.each`
|
||||
testValueA | aType | testValueB | bType | expectEqual
|
||||
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'b' } } | ${ 'object' } | ${ true }
|
||||
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'c' } } | ${ 'object' } | ${ false }
|
||||
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'b', 'bar' ] } | ${ 'array' } | ${ true }
|
||||
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'bar', 'b' ] } | ${ 'array' } | ${ false }
|
||||
${ 1 } | ${ 'number' } | ${ 1 } | ${ 'number' } | ${ true }
|
||||
${ 1 } | ${ 'number' } | ${ '1' } | ${ 'string' } | ${ false }
|
||||
${ '1' } | ${ 'string' } | ${ '1' } | ${ 'string' } | ${ true }
|
||||
${ 1 } | ${ 'number' } | ${ 2 } | ${ 'number' } | ${ false }
|
||||
${ 1 } | ${ 'number' } | ${ true } | ${ 'bool' } | ${ false }
|
||||
${ 0 } | ${ 'number' } | ${ false } | ${ 'bool' } | ${ false }
|
||||
${ true } | ${ 'bool' } | ${ true } | ${ 'bool' } | ${ true }
|
||||
testValueA | aType | testValueB | bType
|
||||
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'b' } } | ${ 'object' }
|
||||
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'b', 'bar' ] } | ${ 'array' }
|
||||
${ 1 } | ${ 'number' } | ${ 1 } | ${ 'number' }
|
||||
${ '1' } | ${ 'string' } | ${ '1' } | ${ 'string' }
|
||||
${ true } | ${ 'bool' } | ${ true } | ${ 'bool' }
|
||||
`(
|
||||
'$testValueA ($aType) and $testValueB ($bType) are expected to be equal ($expectEqual)',
|
||||
( { testValueA, testValueB, expectEqual } ) => {
|
||||
'$testValueA ($aType) and $testValueB ($bType) are expected to be equal',
|
||||
( { testValueA, testValueB } ) => {
|
||||
let testPropValue;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
|
@ -44,11 +38,35 @@ describe( 'useShallowEqual', () => {
|
|||
renderer.update( <TestComponent testValue={ testValueB } /> );
|
||||
} );
|
||||
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
|
||||
if ( expectEqual ) {
|
||||
expect( testPropValue ).toBe( testValueA );
|
||||
} else {
|
||||
expect( testPropValue ).toBe( testValueB );
|
||||
}
|
||||
expect( testPropValue ).toBe( testValueA );
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
testValueA | aType | testValueB | bType
|
||||
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'c' } } | ${ 'object' }
|
||||
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'bar', 'b' ] } | ${ 'array' }
|
||||
${ 1 } | ${ 'number' } | ${ '1' } | ${ 'string' }
|
||||
${ 1 } | ${ 'number' } | ${ 2 } | ${ 'number' }
|
||||
${ 1 } | ${ 'number' } | ${ true } | ${ 'bool' }
|
||||
${ 0 } | ${ 'number' } | ${ false } | ${ 'bool' }
|
||||
`(
|
||||
'$testValueA ($aType) and $testValueB ($bType) are expected to not be equal',
|
||||
( { testValueA, testValueB } ) => {
|
||||
let testPropValue;
|
||||
act( () => {
|
||||
renderer = TestRenderer.create(
|
||||
<TestComponent testValue={ testValueA } />
|
||||
);
|
||||
} );
|
||||
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
|
||||
expect( testPropValue ).toBe( testValueA );
|
||||
// do update
|
||||
act( () => {
|
||||
renderer.update( <TestComponent testValue={ testValueB } /> );
|
||||
} );
|
||||
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
|
||||
expect( testPropValue ).toBe( testValueB );
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {},
|
||||
"include": [ ".", "../../type-defs/**.ts", "../utils/index.js" ],
|
||||
"exclude": [ "**/test/**" ]
|
||||
}
|
|
@ -29,7 +29,7 @@ import { useResizeObserver } from 'wordpress-compose';
|
|||
* };
|
||||
* ```
|
||||
*/
|
||||
export const useContainerQueries = () => {
|
||||
export const useContainerQueries = (): [ React.ReactElement, string ] => {
|
||||
const [ resizeListener, { width } ] = useResizeObserver();
|
||||
|
||||
let className = '';
|
|
@ -3,6 +3,9 @@
|
|||
*/
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
interface Validation< T > {
|
||||
( value: T, previousValue: T | undefined ): boolean;
|
||||
}
|
||||
/**
|
||||
* Use Previous based on https://usehooks.com/usePrevious/.
|
||||
*
|
||||
|
@ -10,8 +13,11 @@ import { useRef, useEffect } from 'react';
|
|||
* @param {Function} [validation] Function that needs to validate for the value
|
||||
* to be updated.
|
||||
*/
|
||||
export const usePrevious = ( value, validation ) => {
|
||||
const ref = useRef();
|
||||
export function usePrevious< T >(
|
||||
value: T,
|
||||
validation?: Validation< T >
|
||||
): T | undefined {
|
||||
const ref = useRef< T >();
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
|
@ -23,4 +29,4 @@ export const usePrevious = ( value, validation ) => {
|
|||
}, [ value, validation ] );
|
||||
|
||||
return ref.current;
|
||||
};
|
||||
}
|
|
@ -16,10 +16,10 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
|
|||
* @return {*} The previous cached instance of the value if the current has
|
||||
* shallow equality with it.
|
||||
*/
|
||||
export const useShallowEqual = ( value ) => {
|
||||
const ref = useRef();
|
||||
if ( ! isShallowEqual( value, ref.current ) ) {
|
||||
export function useShallowEqual< T >( value: T ): T | undefined {
|
||||
const ref = useRef< T >();
|
||||
if ( ref.current === undefined || ! isShallowEqual( value, ref.current ) ) {
|
||||
ref.current = value;
|
||||
}
|
||||
return ref.current;
|
||||
};
|
||||
}
|
|
@ -5,7 +5,7 @@ import { useState, useEffect, useRef } from '@wordpress/element';
|
|||
import { useDispatch } from '@wordpress/data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
import type { CartItem } from '@woocommerce/types';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -16,6 +16,12 @@ import { useStoreCart } from './cart';
|
|||
* @typedef {import('@woocommerce/type-defs/hooks').StoreCartItemAddToCart} StoreCartItemAddToCart
|
||||
*/
|
||||
|
||||
interface StoreAddToCart {
|
||||
cartQuantity: number;
|
||||
addingToCart: boolean;
|
||||
cartIsLoading: boolean;
|
||||
addToCart: ( quantity: number ) => void;
|
||||
}
|
||||
/**
|
||||
* Get the quantity of a product in the cart.
|
||||
*
|
||||
|
@ -23,7 +29,10 @@ import { useStoreCart } from './cart';
|
|||
* @param {number} productId The product id to look for.
|
||||
* @return {number} Quantity in the cart.
|
||||
*/
|
||||
const getQuantityFromCartItems = ( cartItems, productId ) => {
|
||||
const getQuantityFromCartItems = (
|
||||
cartItems: Array< CartItem >,
|
||||
productId: number
|
||||
): number => {
|
||||
const productItem = cartItems.find( ( { id } ) => id === productId );
|
||||
|
||||
return productItem ? productItem.quantity : 0;
|
||||
|
@ -39,7 +48,7 @@ const getQuantityFromCartItems = ( cartItems, productId ) => {
|
|||
* @return {StoreCartItemAddToCart} An object exposing data and actions relating
|
||||
* to add to cart functionality.
|
||||
*/
|
||||
export const useStoreAddToCart = ( productId ) => {
|
||||
export const useStoreAddToCart = ( productId: number ): StoreAddToCart => {
|
||||
const { addItemToCart } = useDispatch( storeKey );
|
||||
const { cartItems, cartIsLoading } = useStoreCart();
|
||||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
|
@ -10,16 +10,11 @@ import { useState, useCallback } from '@wordpress/element';
|
|||
*
|
||||
* @return {function(Object)} A function receiving the error that will be thrown.
|
||||
*/
|
||||
export const useThrowError = () => {
|
||||
export const useThrowError = (): ( ( error: Error ) => void ) => {
|
||||
const [ , setState ] = useState();
|
||||
|
||||
const throwError = useCallback(
|
||||
( error ) =>
|
||||
setState( () => {
|
||||
throw error;
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
return throwError;
|
||||
return useCallback( ( error: Error ): void => {
|
||||
setState( () => {
|
||||
throw error;
|
||||
} );
|
||||
}, [] );
|
||||
};
|
|
@ -9,8 +9,8 @@ import { triggerFragmentRefresh } from '@woocommerce/base-utils';
|
|||
*
|
||||
* @param {number} quantityInCart Quantity of the item in the cart.
|
||||
*/
|
||||
export const useTriggerFragmentRefresh = ( quantityInCart ) => {
|
||||
const firstMount = useRef( true );
|
||||
export const useTriggerFragmentRefresh = ( quantityInCart: number ): void => {
|
||||
const firstMount = useRef< boolean >( true );
|
||||
|
||||
useEffect( () => {
|
||||
if ( firstMount.current ) {
|
|
@ -1,3 +1,8 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { fromEntriesPolyfill } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Get an array of selected shipping rates keyed by Package ID.
|
||||
*
|
||||
|
@ -5,15 +10,11 @@
|
|||
* @return {Object} Object containing the package IDs and selected rates in the format: { [packageId:string]: rateId:string }
|
||||
*/
|
||||
export const deriveSelectedShippingRates = ( shippingRates ) =>
|
||||
shippingRates
|
||||
.map( ( { package_id: packageId, shipping_rates: packageRates } ) => [
|
||||
packageId,
|
||||
packageRates.find( ( rate ) => rate.selected )?.rate_id,
|
||||
] )
|
||||
// A fromEntries polyfill, creates an object from an array of arrays.
|
||||
.reduce( ( obj, [ key, val ] ) => {
|
||||
if ( val ) {
|
||||
obj[ key ] = val;
|
||||
}
|
||||
return obj;
|
||||
}, {} );
|
||||
fromEntriesPolyfill(
|
||||
shippingRates.map(
|
||||
( { package_id: packageId, shipping_rates: packageRates } ) => [
|
||||
packageId,
|
||||
packageRates.find( ( rate ) => rate.selected )?.rate_id,
|
||||
]
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* A polyfill for Object.fromEntries function.
|
||||
*
|
||||
* @param {Array<[string, unknown]>} array Array to be turned back to object
|
||||
* @return {Record< string, unknown >} the newly created object
|
||||
*/
|
||||
export const fromEntriesPolyfill = (
|
||||
array: Array< [ string, unknown ] >
|
||||
): Record< string, unknown > =>
|
||||
array.reduce< Record< string, unknown > >( ( obj, [ key, val ] ) => {
|
||||
if ( val ) {
|
||||
obj[ key ] = val;
|
||||
}
|
||||
return obj;
|
||||
}, {} );
|
|
@ -7,3 +7,4 @@ export * from './get-intersection-observer';
|
|||
export * from './get-valid-block-attributes';
|
||||
export * from './product-data';
|
||||
export * from './derive-selected-shipping-rates';
|
||||
export * from './from-entries-polyfill';
|
||||
|
|
|
@ -18,6 +18,7 @@ import { ACTION_TYPES as types } from './action-types';
|
|||
import { STORE_KEY as CART_STORE_KEY } from './constants';
|
||||
import { apiFetchWithHeaders } from '../shared-controls';
|
||||
import type { ResponseError } from '../types';
|
||||
import { ReturnOrGeneratorYieldUnion } from '../../mapped-types';
|
||||
|
||||
/**
|
||||
* Returns an action object used in updating the store with the provided items
|
||||
|
@ -424,7 +425,7 @@ export function* updateCustomerData(
|
|||
return true;
|
||||
}
|
||||
|
||||
export type CartAction = ReturnType<
|
||||
export type CartAction = ReturnOrGeneratorYieldUnion<
|
||||
| typeof receiveCart
|
||||
| typeof receiveError
|
||||
| typeof receiveApplyingCoupon
|
||||
|
@ -434,4 +435,8 @@ export type CartAction = ReturnType<
|
|||
| typeof itemIsPendingDelete
|
||||
| typeof updatingCustomerData
|
||||
| typeof shippingRatesBeingSelected
|
||||
| typeof updateCustomerData
|
||||
| typeof removeItemFromCart
|
||||
| typeof changeCartItemQuantity
|
||||
| typeof addItemToCart
|
||||
>;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { registerStore } from '@wordpress/data';
|
||||
import { controls as dataControls } from '@wordpress/data-controls';
|
||||
import type { DispatchFromMap, SelectFromMap } from '@automattic/data-stores';
|
||||
import type { SelectFromMap } from '@automattic/data-stores';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -14,6 +14,7 @@ import * as actions from './actions';
|
|||
import * as resolvers from './resolvers';
|
||||
import reducer, { State } from './reducers';
|
||||
import { controls } from '../shared-controls';
|
||||
import { DispatchFromMap } from '../../mapped-types';
|
||||
|
||||
registerStore< State >( STORE_KEY, {
|
||||
reducer,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {},
|
||||
"include": [ ".", "../type-defs/**.ts" ],
|
||||
"include": [ ".", "../type-defs/**.ts", "../mapped-types.ts" ],
|
||||
"exclude": [ "**/test/**" ]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Usually we use ReturnType of all the action creators to deduce all the actions.
|
||||
* This works until one of the action creators is a generator and doesn't actually "Return" an action.
|
||||
* This type helper allows for actions to be both functions and generators
|
||||
*/
|
||||
export type ReturnOrGeneratorYieldUnion<
|
||||
T extends ( ...args: any ) => any
|
||||
> = T extends ( ...args: any ) => infer Return
|
||||
? Return extends Generator< infer T, infer U, any >
|
||||
? T | U
|
||||
: Return
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Maps a "raw" actionCreators object to the actions available when registered on the @wordpress/data store.
|
||||
*
|
||||
* @template A Selector map, usually from `import * as actions from './my-store/actions';`
|
||||
*/
|
||||
// Todo: This is a temporary implementation of what exists in @automattic/data-stores
|
||||
// already: https://github.com/Automattic/wp-calypso/blob/0ba7f5a91571b3ba6a84be7db021c7eab247e434/packages/data-stores/src/mapped-types.ts
|
||||
// We will need to remove this when @automattic/data-stores is updated on NPM.
|
||||
export type DispatchFromMap<
|
||||
A extends Record< string, ( ...args: any[] ) => any >
|
||||
> = {
|
||||
[ actionCreator in keyof A ]: (
|
||||
...args: Parameters< A[ actionCreator ] >
|
||||
) => A[ actionCreator ] extends ( ...args: any[] ) => Generator
|
||||
? Promise< GeneratorReturnType< A[ actionCreator ] > >
|
||||
: void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtain the type finally returned by the generator when it's done iterating.
|
||||
*/
|
||||
// Todo: This is a temporary implementation of what exists in @automattic/data-stores
|
||||
// already: https://github.com/Automattic/wp-calypso/blob/0ba7f5a91571b3ba6a84be7db021c7eab247e434/packages/data-stores/src/mapped-types.ts
|
||||
// We will need to remove this when @automattic/data-stores is updated on NPM.
|
||||
export type GeneratorReturnType<
|
||||
T extends ( ...args: any[] ) => Generator
|
||||
> = T extends ( ...args: any ) => Generator< any, infer R, any > ? R : never;
|
|
@ -7,6 +7,7 @@ import {
|
|||
WC_BLOCKS_ASSET_URL,
|
||||
SHIPPING_ENABLED,
|
||||
} from '@woocommerce/block-settings';
|
||||
import { CartResponse } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -16,7 +17,7 @@ import { previewShippingRates } from './shipping-rates';
|
|||
// Sample data for cart block.
|
||||
// This closely resembles the data returned from the Store API /cart endpoint.
|
||||
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/trunk/src/RestApi/StoreApi#cart-api
|
||||
export const previewCart = {
|
||||
export const previewCart: CartResponse = {
|
||||
coupons: [],
|
||||
shipping_rates: SHIPPING_METHODS_EXIST ? previewShippingRates : [],
|
||||
items: [
|
||||
|
@ -162,6 +163,30 @@ export const previewCart = {
|
|||
needs_payment: true,
|
||||
needs_shipping: SHIPPING_ENABLED,
|
||||
has_calculated_shipping: true,
|
||||
shipping_address: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
},
|
||||
billing_address: {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
},
|
||||
totals: {
|
||||
currency_code: 'USD',
|
||||
currency_symbol: '$',
|
|
@ -2,10 +2,18 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, _x } from '@wordpress/i18n';
|
||||
import type { CartResponseShippingRateItem } from '@woocommerce/types';
|
||||
|
||||
export const previewShippingRates = [
|
||||
export const previewShippingRates: CartResponseShippingRateItem[] = [
|
||||
{
|
||||
destination: {},
|
||||
destination: {
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
},
|
||||
package_id: 0,
|
||||
name: __( 'Shipping', 'woo-gutenberg-products-block' ),
|
||||
items: [
|
||||
|
@ -41,7 +49,10 @@ export const previewShippingRates = [
|
|||
description: '',
|
||||
delivery_time: '',
|
||||
price: '000',
|
||||
taxes: '0',
|
||||
rate_id: 'free_shipping:1',
|
||||
instance_id: 0,
|
||||
meta_data: [],
|
||||
method_id: 'flat_rate',
|
||||
selected: true,
|
||||
},
|
||||
|
@ -57,7 +68,10 @@ export const previewShippingRates = [
|
|||
description: '',
|
||||
delivery_time: '',
|
||||
price: '200',
|
||||
taxes: '0',
|
||||
rate_id: 'local_pickup:1',
|
||||
instance_id: 1,
|
||||
meta_data: [],
|
||||
method_id: 'local_pickup',
|
||||
selected: false,
|
||||
},
|
|
@ -145,11 +145,11 @@ export interface CartResponseFeeItemTotals extends CurrencyResponseInfo {
|
|||
total_tax: string;
|
||||
}
|
||||
|
||||
export interface CartResponseFeeItem {
|
||||
export type CartResponseFeeItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
totals: CartResponseFeeItemTotals;
|
||||
}
|
||||
};
|
||||
|
||||
export interface CartResponseTotals extends CurrencyResponseInfo {
|
||||
total_items: string;
|
||||
|
@ -186,7 +186,7 @@ export interface CartResponse {
|
|||
needs_shipping: boolean;
|
||||
has_calculated_shipping: boolean;
|
||||
fees: Array< CartResponseFeeItem >;
|
||||
totals: CartResponseTotalsItem;
|
||||
totals: CartResponseTotals;
|
||||
errors: Array< CartResponseErrorItem >;
|
||||
payment_requirements: Array< unknown >;
|
||||
extensions: Array< CartResponseExtensionItem >;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type {
|
||||
CartResponseErrorItem,
|
||||
CartResponseCouponItem,
|
||||
CartResponseItem,
|
||||
CartResponseFeeItem,
|
||||
CartResponseTotals,
|
||||
CartResponseShippingAddress,
|
||||
CartResponseBillingAddress,
|
||||
CartResponseShippingRateItem,
|
||||
CartResponse,
|
||||
} from './cart-response';
|
||||
import type { ResponseError } from '../data/types';
|
||||
export interface StoreCartItemQuantity {
|
||||
isPendingDelete: boolean;
|
||||
quantity: number;
|
||||
changeQuantity: ( quantity: number ) => void;
|
||||
removeItem: () => Promise< void > | false;
|
||||
cartItemQuantityErrors: Array< CartResponseErrorItem >;
|
||||
}
|
||||
|
||||
export interface StoreCartCoupon {
|
||||
appliedCoupons: Array< CartResponseCouponItem >;
|
||||
isLoading: boolean;
|
||||
applyCoupon: ( coupon: string ) => void;
|
||||
removeCoupon: ( coupon: string ) => void;
|
||||
isApplyingCoupon: boolean;
|
||||
isRemovingCoupon: boolean;
|
||||
}
|
||||
|
||||
export interface StoreCart {
|
||||
cartCoupons: Array< CartResponseCouponItem >;
|
||||
cartItems: Array< CartResponseItem >;
|
||||
cartFees: Array< CartResponseFeeItem >;
|
||||
cartItemsCount: number;
|
||||
cartItemsWeight: number;
|
||||
cartNeedsPayment: boolean;
|
||||
cartNeedsShipping: boolean;
|
||||
cartItemErrors: Array< CartResponseErrorItem >;
|
||||
cartTotals: CartResponseTotals;
|
||||
cartIsLoading: boolean;
|
||||
cartErrors: Array< ResponseError >;
|
||||
billingAddress: CartResponseBillingAddress;
|
||||
shippingAddress: CartResponseShippingAddress;
|
||||
shippingRates: Array< CartResponseShippingRateItem >;
|
||||
extensions: Record< string, unknown >;
|
||||
shippingRatesLoading: boolean;
|
||||
cartHasCalculatedShipping: boolean;
|
||||
paymentRequirements: Array< string >;
|
||||
receiveCart: ( cart: CartResponse ) => void;
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from './cart-response';
|
||||
export * from './cart';
|
||||
export * from './hooks';
|
||||
|
|
Loading…
Reference in New Issue