Storybook and TS migration of some cart checkout components (https://github.com/woocommerce/woocommerce-blocks/pull/5324)

* Migrate stories for `ProductName` to latest Storybook. Also add props
documentation and add named export.
* Migrate `TotalsFooterItem` to TypeScript and latest Storybook
* Add a `LooselyMustHave` utility type.
* Export `allSettings` so that they can be manipulated in stories and tests

* Implement a way to easily define and reuse Storybook controls

Implement a currency control for a common use-case of selecting currencies.
It currently implements EUR and USD as they have different properties.

* Migrate `TotalsDiscount` to TypeScript and implement stories
* Migrate `TotalsCoupon` to TypeScript and fix stories
* Change Coupon name within Storybook
* Nicer handling of removal of a coupon from Storybook

It now dynamically calculates the discount from the actual coupons.
This commit is contained in:
M. L. Giannotta 2021-12-15 17:30:17 +01:00 committed by GitHub
parent 91c2ca2143
commit 703051b1bc
25 changed files with 460 additions and 279 deletions

View File

@ -7,7 +7,6 @@ import Button from '@woocommerce/base-components/button';
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
import Label from '@woocommerce/base-components/label';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import PropTypes from 'prop-types';
import { withInstanceId } from '@wordpress/compose';
import {
ValidationInputError,
@ -20,12 +19,31 @@ import { Panel } from '@woocommerce/blocks-checkout';
*/
import './style.scss';
const TotalsCoupon = ( {
export interface TotalsCouponProps {
/**
* Instance id of the input
*/
instanceId: string;
/**
* Whether the component is in a loading state
*/
isLoading?: boolean;
/**
* Whether the component's parent panel will begin in an open state
*/
initialOpen?: boolean;
/**
* Submit handler
*/
onSubmit?: ( couponValue: string ) => void;
}
export const TotalsCoupon = ( {
instanceId,
isLoading = false,
initialOpen = false,
onSubmit = () => {},
} ) => {
onSubmit = () => void 0,
}: TotalsCouponProps ): JSX.Element => {
const [ couponValue, setCouponValue ] = useState( '' );
const currentIsLoading = useRef( false );
const { getValidationError, getValidationErrorId } = useValidationContext();
@ -94,7 +112,9 @@ const TotalsCoupon = ( {
className="wc-block-components-totals-coupon__button"
disabled={ isLoading || ! couponValue }
showSpinner={ isLoading }
onClick={ ( e ) => {
onClick={ (
e: React.MouseEvent< HTMLElement, 'click' >
) => {
e.preventDefault();
onSubmit( couponValue );
} }
@ -113,9 +133,4 @@ const TotalsCoupon = ( {
);
};
TotalsCoupon.propTypes = {
onSubmit: PropTypes.func,
isLoading: PropTypes.bool,
};
export default withInstanceId( TotalsCoupon );

View File

@ -1,47 +0,0 @@
/**
* External dependencies
*/
import { text, boolean } from '@storybook/addon-knobs';
import {
useValidationContext,
ValidationContextProvider,
} from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import TotalsCoupon from '../';
export default {
title:
'WooCommerce Blocks/@base-components/cart-checkout/totals/TotalsCoupon',
component: TotalsCoupon,
};
const StoryComponent = ( { validCoupon, isLoading, invalidCouponText } ) => {
const { setValidationErrors } = useValidationContext();
const onSubmit = ( coupon ) => {
if ( coupon !== validCoupon ) {
setValidationErrors( { coupon: invalidCouponText } );
}
};
return <TotalsCoupon isLoading={ isLoading } onSubmit={ onSubmit } />;
};
export const Default = () => {
const validCoupon = text( 'A valid coupon code', 'validcoupon' );
const invalidCouponText = text(
'Error message for invalid code',
'Invalid coupon code.'
);
const isLoading = boolean( 'Toggle isLoading state', false );
return (
<ValidationContextProvider>
<StoryComponent
validCoupon={ validCoupon }
isLoading={ isLoading }
invalidCouponText={ invalidCouponText }
/>
</ValidationContextProvider>
);
};

View File

@ -0,0 +1,70 @@
/**
* External dependencies
*/
import { useArgs } from '@storybook/client-api';
import { Story, Meta } from '@storybook/react';
import {
useValidationContext,
ValidationContextProvider,
} from '@woocommerce/base-context';
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
/**
* Internal dependencies
*/
import { TotalsCoupon, TotalsCouponProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/totals/Coupon',
component: TotalsCoupon,
args: {
initialOpen: true,
},
} as Meta< TotalsCouponProps >;
const INVALID_COUPON_ERROR = {
hidden: false,
message: 'Invalid coupon code',
};
const Template: Story< TotalsCouponProps > = ( args ) => {
const [ {}, setArgs ] = useArgs();
const onSubmit = ( code: string ) => {
args.onSubmit?.( code );
setArgs( { isLoading: true } );
setTimeout(
() => setArgs( { isLoading: false } ),
INTERACTION_TIMEOUT
);
};
return <TotalsCoupon { ...args } onSubmit={ onSubmit } />;
};
export const Default = Template.bind( {} );
Default.args = {};
export const LoadingState = Template.bind( {} );
LoadingState.args = {
isLoading: true,
};
export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
const { setValidationErrors } = useValidationContext();
setValidationErrors( { coupon: INVALID_COUPON_ERROR } );
return <TotalsCoupon { ...args } />;
};
ErrorState.decorators = [
( StoryComponent ) => {
return (
<ValidationContextProvider>
<StoryComponent />
</ValidationContextProvider>
);
},
];

View File

@ -4,18 +4,37 @@
import { __, sprintf } from '@wordpress/i18n';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { RemovableChip } from '@woocommerce/base-components/chip';
import PropTypes from 'prop-types';
import {
__experimentalApplyCheckoutFilter,
TotalsItem,
} from '@woocommerce/blocks-checkout';
import { getSetting } from '@woocommerce/settings';
import {
CartResponseCouponItemWithLabel,
CartTotalsItem,
Currency,
} from '@woocommerce/types';
import { LooselyMustHave } from '@woocommerce/type-defs/utils';
/**
* Internal dependencies
*/
import './style.scss';
export interface TotalsDiscountProps {
cartCoupons: LooselyMustHave<
CartResponseCouponItemWithLabel,
'code' | 'label' | 'totals'
>[];
currency: Currency;
isRemovingCoupon: boolean;
removeCoupon: ( couponCode: string ) => void;
values: LooselyMustHave<
CartTotalsItem,
'total_discount' | 'total_discount_tax'
>;
}
const filteredCartCouponsFilterArg = {
context: 'summary',
};
@ -26,7 +45,7 @@ const TotalsDiscount = ( {
isRemovingCoupon,
removeCoupon,
values,
} ) => {
}: TotalsDiscountProps ): JSX.Element | null => {
const {
total_discount: totalDiscount,
total_discount_tax: totalDiscountTax,
@ -110,19 +129,4 @@ const TotalsDiscount = ( {
);
};
TotalsDiscount.propTypes = {
cartCoupons: PropTypes.arrayOf(
PropTypes.shape( {
code: PropTypes.string.isRequired,
} )
),
currency: PropTypes.object.isRequired,
isRemovingCoupon: PropTypes.bool.isRequired,
removeCoupon: PropTypes.func.isRequired,
values: PropTypes.shape( {
total_discount: PropTypes.string,
total_discount_tax: PropTypes.string,
} ).isRequired,
};
export default TotalsDiscount;

View File

@ -1,37 +0,0 @@
/**
* External dependencies
*/
import { text, boolean } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import TotalsDiscount from '../';
export default {
title:
'WooCommerce Blocks/@base-components/cart-checkout/totals/TotalsDiscount',
component: TotalsDiscount,
};
export const Default = () => {
const cartCoupons = [ { code: 'COUPON' } ];
const currency = currencyKnob();
const isRemovingCoupon = boolean( 'Toggle isRemovingCoupon state', false );
const totalDiscount = text( 'Total discount', '1000' );
const totalDiscountTax = text( 'Total discount tax', '200' );
return (
<TotalsDiscount
cartCoupons={ cartCoupons }
currency={ currency }
isRemovingCoupon={ isRemovingCoupon }
removeCoupon={ () => void null }
values={ {
total_discount: totalDiscount,
total_discount_tax: totalDiscountTax,
} }
/>
);
};

View File

@ -0,0 +1,111 @@
/**
* External dependencies
*/
import { useArgs } from '@storybook/client-api';
import { Story, Meta } from '@storybook/react';
import {
currenciesAPIShape as currencies,
currencyControl,
INTERACTION_TIMEOUT,
} from '@woocommerce/storybook-controls';
import { LooselyMustHave } from '@woocommerce/type-defs/utils';
import {
CartResponseCouponItemWithLabel,
CartTotalsItem,
} from '@woocommerce/types';
/**
* Internal dependencies
*/
import Discount, { TotalsDiscountProps } from '..';
const EXAMPLE_COUPONS: CartResponseCouponItemWithLabel[] = [
{
code: 'AWSMSB',
discount_type: '',
label: 'Awesome Storybook coupon',
totals: {
...currencies.EUR,
total_discount: '5000',
total_discount_tax: '250',
},
},
{
code: 'STONKS',
discount_type: '',
label: 'Most valuable coupon',
totals: {
...currencies.EUR,
total_discount: '10000',
total_discount_tax: '1000',
},
},
];
function extractValuesFromCoupons(
coupons: LooselyMustHave< CartResponseCouponItemWithLabel, 'totals' >[]
) {
return coupons.reduce(
( acc, curr ) => {
const totalDiscount =
Number( acc.total_discount ) +
Number( curr.totals.total_discount );
const totalDiscountTax =
Number( acc.total_discount_tax ) +
Number( curr.totals.total_discount_tax );
return {
total_discount: String( totalDiscount ),
total_discount_tax: String( totalDiscountTax ),
};
},
{ total_discount: '0', total_discount_tax: '0' } as LooselyMustHave<
CartTotalsItem,
'total_discount' | 'total_discount_tax'
>
);
}
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/totals/Discount',
component: Discount,
argTypes: {
currency: currencyControl,
removeCoupon: { action: 'Removing coupon with code' },
},
args: {
cartCoupons: EXAMPLE_COUPONS,
isRemovingCoupon: false,
values: extractValuesFromCoupons( EXAMPLE_COUPONS ),
},
} as Meta< TotalsDiscountProps >;
const Template: Story< TotalsDiscountProps > = ( args ) => {
const [ {}, setArgs ] = useArgs();
const removeCoupon = ( code: string ) => {
args.removeCoupon( code );
setArgs( { isRemovingCoupon: true } );
const cartCoupons = args.cartCoupons.filter(
( coupon ) => coupon.code !== code
);
const values = extractValuesFromCoupons( cartCoupons );
setTimeout(
() => setArgs( { cartCoupons, values, isRemovingCoupon: false } ),
INTERACTION_TIMEOUT
);
};
return <Discount { ...args } removeCoupon={ removeCoupon } />;
};
export const Default = Template.bind( {} );
Default.args = {};
export const RemovingCoupon = Template.bind( {} );
RemovingCoupon.args = {
isRemovingCoupon: true,
};

View File

@ -4,23 +4,48 @@
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import PropTypes from 'prop-types';
import {
__experimentalApplyCheckoutFilter,
TotalsItem,
} from '@woocommerce/blocks-checkout';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
import { CartResponseTotals, Currency } from '@woocommerce/types';
import { LooselyMustHave } from '@woocommerce/type-defs/utils';
/**
* Internal dependencies
*/
import './style.scss';
const TotalsFooterItem = ( { currency, values } ) => {
export interface TotalsFooterItemProps {
/**
* The currency object with which to display the item
*/
currency: Currency;
/**
* An object containing the total price and the total tax
*
* It accepts the entire `CartResponseTotals` to be passed, for
* convenience, but will use only these two properties.
*/
values: LooselyMustHave< CartResponseTotals, 'total_price' | 'total_tax' >;
}
/**
* The total at the bottom of the cart
*
* Can show how much of the total is in taxes if the settings
* `taxesEnabled` and `displayCartPricesIncludingTax` are both
* enabled.
*/
const TotalsFooterItem = ( {
currency,
values,
}: TotalsFooterItemProps ): JSX.Element => {
const SHOW_TAXES =
getSetting( 'taxesEnabled', true ) &&
getSetting( 'displayCartPricesIncludingTax', false );
getSetting< boolean >( 'taxesEnabled', true ) &&
getSetting< boolean >( 'displayCartPricesIncludingTax', false );
const { total_price: totalPrice, total_tax: totalTax } = values;
@ -69,12 +94,4 @@ const TotalsFooterItem = ( { currency, values } ) => {
);
};
TotalsFooterItem.propTypes = {
currency: PropTypes.object.isRequired,
values: PropTypes.shape( {
total_price: PropTypes.string,
total_tax: PropTypes.string,
} ).isRequired,
};
export default TotalsFooterItem;

View File

@ -1,32 +0,0 @@
/**
* External dependencies
*/
import { text } from '@storybook/addon-knobs';
import { currencyKnob } from '@woocommerce/knobs';
/**
* Internal dependencies
*/
import TotalsFooterItem from '../';
export default {
title:
'WooCommerce Blocks/@base-components/cart-checkout/totals/TotalsFooterItem',
component: TotalsFooterItem,
};
export const Default = () => {
const currency = currencyKnob();
const totalPrice = text( 'Total price', '1200' );
const totalTax = text( 'Total tax', '200' );
return (
<TotalsFooterItem
currency={ currency }
values={ {
total_price: totalPrice,
total_tax: totalTax,
} }
/>
);
};

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
import { allSettings } from '@woocommerce/settings';
import { Currency } from '@woocommerce/types';
/**
* Internal dependencies
*/
import FooterItem, { TotalsFooterItemProps } from '..';
const NZD: Currency = {
code: 'nzd',
symbol: '$',
thousandSeparator: ' ',
decimalSeparator: '.',
minorUnit: 2,
prefix: '$',
suffix: '',
};
export default {
title:
'WooCommerce Blocks/@base-components/cart-checkout/totals/FooterItem',
component: FooterItem,
args: {
currency: NZD,
values: { total_price: '2500', total_tax: '550' },
},
} as Meta< TotalsFooterItemProps >;
const Template: Story< TotalsFooterItemProps > = ( args ) => (
<FooterItem { ...args } />
);
export const Default = Template.bind( {} );
Default.decorators = [
( StoryComponent ) => {
allSettings.displayCartPricesIncludingTax = false;
return <StoryComponent />;
},
];
export const IncludingTaxes = Template.bind( {} );
IncludingTaxes.decorators = [
( StoryComponent ) => {
allSettings.displayCartPricesIncludingTax = true;
return <StoryComponent />;
},
];

View File

@ -1,79 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TotalsFooterItem Does not show the "including %s of tax" line if tax is 0 1`] = `
<div>
<div
class="wc-block-components-totals-item wc-block-components-totals-footer-item"
>
<span
class="wc-block-components-totals-item__label"
>
Total
</span>
<span
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-item__value"
>
£85.00
</span>
<div
class="wc-block-components-totals-item__description"
/>
</div>
</div>
`;
exports[`TotalsFooterItem Does not show the "including %s of tax" line if tax is disabled 1`] = `
<div>
<div
class="wc-block-components-totals-item wc-block-components-totals-footer-item"
>
<span
class="wc-block-components-totals-item__label"
>
Total
</span>
<span
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-item__value"
>
£85.00
</span>
<div
class="wc-block-components-totals-item__description"
/>
</div>
</div>
`;
exports[`TotalsFooterItem Shows the "including %s of tax" line if tax is greater than 0 1`] = `
<div>
<div
class="wc-block-components-totals-item wc-block-components-totals-footer-item"
>
<span
class="wc-block-components-totals-item__label"
>
Total
</span>
<span
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-item__value"
>
£85.00
</span>
<div
class="wc-block-components-totals-item__description"
>
<p
class="wc-block-components-totals-footer-item-tax"
>
Including
<span
class="wc-block-formatted-money-amount wc-block-components-formatted-money-amount wc-block-components-totals-footer-item-tax-value"
>
£1.00
</span>
in taxes
</p>
</div>
</div>
</div>
`;

View File

@ -2,12 +2,12 @@
* External dependencies
*/
import { render } from '@testing-library/react';
import { allSettings } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import TotalsFooterItem from '../index';
import { allSettings } from '../../../../../../settings/shared/settings-init';
describe( 'TotalsFooterItem', () => {
beforeEach( () => {

View File

@ -10,11 +10,26 @@ import { AnchorHTMLAttributes, HTMLAttributes } from 'react';
*/
import './style.scss';
interface ProductNameProps extends AnchorHTMLAttributes< HTMLAnchorElement > {
export interface ProductNameProps
extends AnchorHTMLAttributes< HTMLAnchorElement > {
/**
* If `true` renders a `span` element instead of a link
*/
disabled?: boolean;
/**
* The product name
*
* Note: can be an HTML string
*/
name: string;
permalink?: string;
/**
* Click handler
*/
onClick?: () => void;
/**
* Link for the product
*/
permalink?: string;
}
/**
@ -22,7 +37,7 @@ interface ProductNameProps extends AnchorHTMLAttributes< HTMLAnchorElement > {
*
* The store API runs titles through `wp_kses_post()` which removes dangerous HTML tags, so using it inside `dangerouslySetInnerHTML` is considered safe.
*/
export default ( {
export const ProductName = ( {
className = '',
disabled = false,
name,
@ -59,3 +74,5 @@ export default ( {
/>
);
};
export default ProductName;

View File

@ -1,26 +0,0 @@
/**
* External dependencies
*/
import { boolean } from '@storybook/addon-knobs';
/**
* Internal dependencies
*/
import ProductName from '../';
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/ProductName',
component: ProductName,
};
export const Default = () => {
const disabled = boolean( 'disabled', false );
return (
<ProductName
disabled={ disabled }
name={ 'Test product' }
permalink={ '/' }
/>
);
};

View File

@ -0,0 +1,32 @@
/**
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
/**
* Internal dependencies
*/
import ProductName, { ProductNameProps } from '..';
export default {
title: 'WooCommerce Blocks/@base-components/cart-checkout/ProductName',
component: ProductName,
args: {
name: 'Test product',
permalink: '#',
},
} as Meta< ProductNameProps >;
const Template: Story< ProductNameProps > = ( args ) => (
<ProductName { ...args } />
);
export const Default = Template.bind( {} );
Default.args = {
disabled: false,
};
export const DisabledProduct = Template.bind( {} );
DisabledProduct.args = {
disabled: true,
};

View File

@ -2,13 +2,14 @@
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {},
"include": [
".",
"../../types",
"../../../../packages/prices",
"../../../../packages/checkout",
"../context",
"../../type-defs",
"../hocs"
".",
"../../types",
"../../../../storybook",
"../../../../packages/prices",
"../../../../packages/checkout",
"../context",
"../../type-defs",
"../hocs"
],
"exclude": [ "**/test/**" ]
}

View File

@ -3,6 +3,7 @@
*/
import { Story, Meta } from '@storybook/react';
import { useArgs } from '@storybook/client-api';
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
/**
* Internal dependencies
@ -21,7 +22,10 @@ const Template: Story< ErrorPlaceholderProps > = ( args ) => {
? () => {
setArgs( { isLoading: true } );
setTimeout( () => setArgs( { isLoading: false } ), 3500 );
setTimeout(
() => setArgs( { isLoading: false } ),
INTERACTION_TIMEOUT
);
}
: undefined;

View File

@ -6,3 +6,4 @@ import '../../filters/exclude-draft-status-from-analytics';
export * from './default-constants';
export * from './default-address-fields';
export * from './utils';
export { allSettings } from './settings-init';

View File

@ -0,0 +1,6 @@
/**
* Allow for the entire object to be passed, with only some properties
* required.
*/
export type LooselyMustHave< T, K extends keyof T > = Partial< T > &
Pick< T, K >;

View File

@ -12928,7 +12928,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
"dev": true,
"requires": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
@ -21164,7 +21163,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
"dev": true,
"requires": {
"tslib": "^2.0.3"
}
@ -21244,8 +21242,7 @@
"map-obj": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz",
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==",
"dev": true
"integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ=="
},
"map-or-similar": {
"version": "1.5.0",
@ -22502,7 +22499,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
"dev": true,
"requires": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
@ -28179,6 +28175,32 @@
"sentence-case": "^1.1.2"
}
},
"snakecase-keys": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-5.1.2.tgz",
"integrity": "sha512-fvtDQZqPBqYb0dEY97TGuOMbN2NJ05Tj4MaoKwjTKkmjcG6mrd58JYGr23UWZRi6Aqv49Fk4HtjTIStOQenaug==",
"requires": {
"map-obj": "^4.1.0",
"snake-case": "^3.0.4",
"type-fest": "^2.5.2"
},
"dependencies": {
"snake-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
"requires": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
"type-fest": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.8.0.tgz",
"integrity": "sha512-O+V9pAshf9C6loGaH0idwsmugI2LxVNR7DtS40gVo2EXZVYFgz9OuNtOhgHLdHdapOEWNdvz9Ob/eeuaWwwlxA=="
}
}
},
"snapdragon": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",

View File

@ -212,6 +212,7 @@
"html-react-parser": "0.14.3",
"react-number-format": "4.4.3",
"reakit": "1.3.11",
"snakecase-keys": "^5.1.2",
"trim-html": "0.1.9",
"use-debounce": "7.0.1",
"wordpress-components": "npm:@wordpress/components@14.2.0"

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { Currency, CurrencyResponse } from '@woocommerce/types';
import snakecaseKeys from 'snakecase-keys';
export const currencies: Record< string, Currency > = {
EUR: {
code: 'EUR',
symbol: '€',
thousandSeparator: '.',
decimalSeparator: ',',
minorUnit: 2,
prefix: '',
suffix: '€',
},
USD: {
code: 'USD',
symbol: '$',
thousandSeparator: ',',
decimalSeparator: '.',
minorUnit: 2,
prefix: '$',
suffix: '',
},
} as const;
export const currenciesAPIShape: Record<
string,
CurrencyResponse
> = Object.fromEntries(
Object.entries( currencies ).map( ( [ key, value ] ) => [
key,
snakecaseKeys( value ),
] )
);
export const currencyControl = {
control: 'select',
defaultValue: currencies.USD,
mapping: currencies,
options: Object.keys( currencies ),
};

View File

@ -0,0 +1,3 @@
export * from './currency';
export const INTERACTION_TIMEOUT = 1500;

View File

@ -55,7 +55,8 @@
"@woocommerce/shared-context": [ "assets/js/shared/context" ],
"@woocommerce/shared-hocs": [ "assets/js/shared/hocs" ],
"@woocommerce/type-defs/*": [ "assets/js/types/type-defs/*" ],
"@woocommerce/types": [ "assets/js/types" ]
"@woocommerce/types": [ "assets/js/types" ],
"@woocommerce/storybook-controls": [ "storybook/custom-controls" ]
}
}
}

View File

@ -3,7 +3,8 @@
"include": [
"./assets/js/**/*",
"./assets/js/blocks/cart-checkout/checkout/inner-blocks/**/block.json",
"./assets/js/blocks/cart-checkout/mini-cart-contents/inner-blocks/**/block.json"
"./assets/js/blocks/cart-checkout/mini-cart-contents/inner-blocks/**/block.json",
"./storybook/**/*"
],
"exclude": [ "./assets/js/data" ],
"references": [