mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-07 19:54:15 +08:00
Merge branch 'refs/heads/trunk' into fix/4022-move-tabpanel-navigation-up
# Conflicts: # modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js
This commit is contained in:
commit
bce124aa8d
68 changed files with 1244 additions and 1114 deletions
|
@ -9,7 +9,12 @@ const ControlButton = ( {
|
|||
buttonLabel,
|
||||
} ) => (
|
||||
<Action>
|
||||
<Button isBusy={ isBusy } variant={ type } onClick={ onClick }>
|
||||
<Button
|
||||
className="small-button"
|
||||
isBusy={ isBusy }
|
||||
variant={ type }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ buttonLabel }
|
||||
</Button>
|
||||
</Action>
|
||||
|
|
|
@ -10,6 +10,7 @@ const ControlTextInput = ( {
|
|||
} ) => (
|
||||
<Action>
|
||||
<TextControl
|
||||
__nextHasNoMarginBottom={ true }
|
||||
className="ppcp-r-vertical-text-control"
|
||||
placeholder={ placeholder }
|
||||
value={ value }
|
||||
|
|
|
@ -24,6 +24,7 @@ const Checkbox = ( {
|
|||
|
||||
return (
|
||||
<CheckboxControl
|
||||
__nextHasNoMarginBottom={ true }
|
||||
label={ label }
|
||||
value={ value }
|
||||
checked={ checked }
|
||||
|
|
|
@ -1,15 +1,39 @@
|
|||
import { PayPalCheckbox } from './index';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
const CheckboxGroup = ( { options, value, onChange } ) => {
|
||||
const handleChange = ( key, checked ) => {
|
||||
const getNewValue = () => {
|
||||
if ( checked ) {
|
||||
return [ ...value, key ];
|
||||
}
|
||||
return value.filter( ( val ) => val !== key );
|
||||
};
|
||||
const CheckboxGroup = ( { name, options, value, onChange } ) => {
|
||||
const handleChange = useCallback(
|
||||
( key, checked ) => {
|
||||
const getNewValue = () => {
|
||||
if ( 'boolean' === typeof value ) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
onChange( getNewValue() );
|
||||
if ( checked ) {
|
||||
return [ ...value, key ];
|
||||
}
|
||||
return value.filter( ( val ) => val !== key );
|
||||
};
|
||||
|
||||
onChange( getNewValue() );
|
||||
},
|
||||
[ onChange, value ]
|
||||
);
|
||||
|
||||
const isItemChecked = ( checked, itemValue ) => {
|
||||
if ( typeof checked === 'boolean' ) {
|
||||
return checked;
|
||||
}
|
||||
|
||||
if ( Array.isArray( value ) ) {
|
||||
return value.includes( itemValue );
|
||||
}
|
||||
|
||||
if ( typeof value === 'boolean' ) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value === itemValue;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -21,16 +45,14 @@ const CheckboxGroup = ( { options, value, onChange } ) => {
|
|||
checked,
|
||||
disabled,
|
||||
description,
|
||||
tooltip,
|
||||
} ) => (
|
||||
<PayPalCheckbox
|
||||
key={ itemValue }
|
||||
key={ name + itemValue }
|
||||
value={ itemValue }
|
||||
label={ label }
|
||||
checked={ checked }
|
||||
checked={ isItemChecked( checked, itemValue ) }
|
||||
disabled={ disabled }
|
||||
description={ description }
|
||||
tooltip={ tooltip }
|
||||
changeCallback={ handleChange }
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -49,10 +49,12 @@ const OptionItem = ( {
|
|||
} ) => {
|
||||
const boxClassName = classNames( 'ppcp-r-select-box', {
|
||||
'ppcp--selected': isSelected,
|
||||
'ppcp--multiselect': isMulti,
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className={ boxClassName }>
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control -- label has a nested input control.
|
||||
<label className={ boxClassName }>
|
||||
<InputField
|
||||
value={ itemValue }
|
||||
isRadio={ ! isMulti }
|
||||
|
@ -60,22 +62,16 @@ const OptionItem = ( {
|
|||
isSelected={ isSelected }
|
||||
/>
|
||||
|
||||
<div className="ppcp-r-select-box__content">
|
||||
<div className="ppcp-r-select-box__content-inner">
|
||||
<span className="ppcp-r-select-box__title">
|
||||
{ itemTitle }
|
||||
</span>
|
||||
<p className="ppcp-r-select-box__description">
|
||||
{ itemDescription }
|
||||
</p>
|
||||
<div className="ppcp--box-content">
|
||||
<div className="ppcp--box-content-inner">
|
||||
<span className="ppcp--box-title">{ itemTitle }</span>
|
||||
<p className="ppcp--box-description">{ itemDescription }</p>
|
||||
{ children && (
|
||||
<div className="ppcp-r-select-box__additional-content">
|
||||
{ children }
|
||||
</div>
|
||||
<div className="ppcp--box-details">{ children }</div>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* Temporary component, until the experimental HStack block editor component is stable.
|
||||
*
|
||||
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs
|
||||
* @file
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
|
||||
const HStack = ( { className, spacing = 3, children } ) => {
|
||||
const wrapperClass = classNames(
|
||||
'components-flex components-h-stack',
|
||||
className
|
||||
);
|
||||
|
||||
const styles = {
|
||||
gap: `calc(${ 4 * spacing }px)`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ wrapperClass } style={ styles }>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HStack;
|
|
@ -1,15 +1,20 @@
|
|||
import { Icon } from '@wordpress/components';
|
||||
|
||||
import data from '../../utils/data';
|
||||
|
||||
const PaymentMethodIcon = ( props ) => {
|
||||
if (
|
||||
( Array.isArray( props.icons ) &&
|
||||
props.icons.includes( props.type ) ) ||
|
||||
props.icons === 'all'
|
||||
) {
|
||||
return data().getImage( 'icon-button-' + props.type + '.svg' );
|
||||
const PaymentMethodIcon = ( { icons, type } ) => {
|
||||
const validIcon = Array.isArray( icons ) && icons.includes( type );
|
||||
|
||||
if ( validIcon || icons === 'all' ) {
|
||||
return (
|
||||
<Icon
|
||||
icon={ data().getImage( 'icon-button-' + type + '.svg' ) }
|
||||
className="ppcp--method-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PaymentMethodIcon;
|
||||
|
|
|
@ -15,22 +15,6 @@ const SettingsBlock = ( {
|
|||
'ppcp--horizontal': horizontalLayout,
|
||||
} );
|
||||
|
||||
const BlockTitle = ( { blockTitle, blockSuffix, blockDescription } ) => {
|
||||
if ( ! blockTitle && ! blockDescription ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Title>
|
||||
{ blockTitle }
|
||||
<TitleExtra>{ blockSuffix }</TitleExtra>
|
||||
</Title>
|
||||
<Description>{ blockDescription }</Description>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ blockClassName }>
|
||||
<BlockTitle
|
||||
|
@ -45,3 +29,19 @@ const SettingsBlock = ( {
|
|||
};
|
||||
|
||||
export default SettingsBlock;
|
||||
|
||||
const BlockTitle = ( { blockTitle, blockSuffix, blockDescription } ) => {
|
||||
if ( ! blockTitle && ! blockDescription ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Header>
|
||||
<Title>
|
||||
{ blockTitle }
|
||||
<TitleExtra>{ blockSuffix }</TitleExtra>
|
||||
</Title>
|
||||
<Description>{ blockDescription }</Description>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<span className="ppcp-r-feature-item__notes">
|
||||
<span className="ppcp--item-notes">
|
||||
{ notes.map( ( note, index ) => (
|
||||
<span key={ index }>{ note }</span>
|
||||
) ) }
|
||||
|
@ -20,6 +20,32 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
const FeatureButton = ( {
|
||||
className,
|
||||
variant,
|
||||
text,
|
||||
isBusy,
|
||||
url,
|
||||
urls,
|
||||
onClick,
|
||||
} ) => {
|
||||
const buttonProps = {
|
||||
className,
|
||||
isBusy,
|
||||
variant,
|
||||
};
|
||||
|
||||
if ( url || urls ) {
|
||||
buttonProps.href = urls ? urls.live : url;
|
||||
buttonProps.target = '_blank';
|
||||
}
|
||||
if ( ! buttonProps.href ) {
|
||||
buttonProps.onClick = onClick;
|
||||
}
|
||||
|
||||
return <Button { ...buttonProps }>{ text }</Button>;
|
||||
};
|
||||
|
||||
const renderDescription = () => {
|
||||
return (
|
||||
<span
|
||||
|
@ -29,32 +55,6 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
);
|
||||
};
|
||||
|
||||
const renderButton = ( button ) => {
|
||||
const buttonElement = (
|
||||
<Button
|
||||
className={ button.class ? button.class : '' }
|
||||
key={ button.text }
|
||||
isBusy={ props.actionProps?.isBusy }
|
||||
variant={ button.type }
|
||||
onClick={ button.onClick }
|
||||
>
|
||||
{ button.text }
|
||||
</Button>
|
||||
);
|
||||
|
||||
// If there's a URL (either direct or in urls object), wrap in anchor tag
|
||||
if ( button.url || button.urls ) {
|
||||
const href = button.urls ? button.urls.live : button.url;
|
||||
return (
|
||||
<a href={ href } key={ button.text }>
|
||||
{ buttonElement }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return buttonElement;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsBlock { ...props } className="ppcp-r-settings-block__feature">
|
||||
<Header>
|
||||
|
@ -70,8 +70,28 @@ const FeatureSettingsBlock = ( { title, description, ...props } ) => {
|
|||
</Description>
|
||||
</Header>
|
||||
<Action>
|
||||
<div className="ppcp-r-feature-item__buttons">
|
||||
{ props.actionProps?.buttons.map( renderButton ) }
|
||||
<div className="ppcp--action-buttons">
|
||||
{ props.actionProps?.buttons.map(
|
||||
( {
|
||||
class: className,
|
||||
type,
|
||||
text,
|
||||
url,
|
||||
urls,
|
||||
onClick,
|
||||
} ) => (
|
||||
<FeatureButton
|
||||
key={ text }
|
||||
className={ className }
|
||||
variant={ type }
|
||||
text={ text }
|
||||
isBusy={ props.actionProps.isBusy }
|
||||
url={ url }
|
||||
urls={ urls }
|
||||
onClick={ onClick }
|
||||
/>
|
||||
)
|
||||
) }
|
||||
</div>
|
||||
</Action>
|
||||
</SettingsBlock>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { ToggleControl } from '@wordpress/components';
|
|||
|
||||
import SettingsBlock from '../SettingsBlock';
|
||||
import PaymentMethodIcon from '../PaymentMethodIcon';
|
||||
import data from '../../../utils/data';
|
||||
|
||||
const PaymentMethodItemBlock = ( {
|
||||
paymentMethod,
|
||||
|
@ -11,33 +10,35 @@ const PaymentMethodItemBlock = ( {
|
|||
isSelected,
|
||||
} ) => {
|
||||
return (
|
||||
<SettingsBlock className="ppcp-r-settings-block__payment-methods__item">
|
||||
<div className="ppcp-r-settings-block__payment-methods__item__inner">
|
||||
<div className="ppcp-r-settings-block__payment-methods__item__title-wrapper">
|
||||
<PaymentMethodIcon
|
||||
icons={ [ paymentMethod.icon ] }
|
||||
type={ paymentMethod.icon }
|
||||
/>
|
||||
<span className="ppcp-r-settings-block__payment-methods__item__title">
|
||||
<SettingsBlock className="ppcp--method-item" separatorAndGap={ false }>
|
||||
<div className="ppcp--method-inner">
|
||||
<div className="ppcp--method-title-wrapper">
|
||||
{ paymentMethod?.icon && (
|
||||
<PaymentMethodIcon
|
||||
icons={ [ paymentMethod.icon ] }
|
||||
type={ paymentMethod.icon }
|
||||
/>
|
||||
) }
|
||||
<span className="ppcp--method-title">
|
||||
{ paymentMethod.itemTitle }
|
||||
</span>
|
||||
</div>
|
||||
<p className="ppcp-r-settings-block__payment-methods__item__description">
|
||||
<p className="ppcp--method-description">
|
||||
{ paymentMethod.itemDescription }
|
||||
</p>
|
||||
<div className="ppcp-r-settings-block__payment-methods__item__footer">
|
||||
<div className="ppcp--method-footer">
|
||||
<ToggleControl
|
||||
__nextHasNoMarginBottom={ true }
|
||||
checked={ isSelected }
|
||||
onChange={ onSelect }
|
||||
/>
|
||||
{ paymentMethod?.fields && onTriggerModal && (
|
||||
<div
|
||||
className="ppcp-r-settings-block__payment-methods__item__settings"
|
||||
<Button
|
||||
className="ppcp--method-settings"
|
||||
onClick={ onTriggerModal }
|
||||
>
|
||||
{ data().getImage( 'icon-settings.svg' ) }
|
||||
</div>
|
||||
<Icon icon={ cog } />
|
||||
</Button>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,42 +1,38 @@
|
|||
import SettingsBlock from '../SettingsBlock';
|
||||
import PaymentMethodItemBlock from './PaymentMethodItemBlock';
|
||||
import { usePaymentMethods } from '../../../data/payment/hooks';
|
||||
import { PaymentHooks } from '../../../data';
|
||||
|
||||
const PaymentMethodsBlock = ( {
|
||||
paymentMethods,
|
||||
className = '',
|
||||
onTriggerModal,
|
||||
} ) => {
|
||||
const { setPersistent } = usePaymentMethods();
|
||||
// TODO: This is not a reusable component, as it's connected to the Redux store.
|
||||
const PaymentMethodsBlock = ( { paymentMethods = [], onTriggerModal } ) => {
|
||||
const { changePaymentSettings } = PaymentHooks.useStore();
|
||||
|
||||
if ( ! paymentMethods?.length ) {
|
||||
const handleSelect = ( methodId, isSelected ) =>
|
||||
changePaymentSettings( methodId, {
|
||||
enabled: isSelected,
|
||||
} );
|
||||
|
||||
if ( ! paymentMethods.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelect = ( paymentMethod, isSelected ) => {
|
||||
setPersistent( paymentMethod.id, {
|
||||
...paymentMethod,
|
||||
enabled: isSelected,
|
||||
} );
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsBlock
|
||||
className={ `ppcp-r-settings-block__payment-methods ${ className }` }
|
||||
>
|
||||
{ paymentMethods.map( ( paymentMethod ) => (
|
||||
<PaymentMethodItemBlock
|
||||
key={ paymentMethod.id }
|
||||
paymentMethod={ paymentMethod }
|
||||
isSelected={ paymentMethod.enabled }
|
||||
onSelect={ ( checked ) =>
|
||||
handleSelect( paymentMethod, checked )
|
||||
}
|
||||
onTriggerModal={ () =>
|
||||
onTriggerModal?.( paymentMethod.id )
|
||||
}
|
||||
/>
|
||||
) ) }
|
||||
<SettingsBlock className="ppcp--grid ppcp-r-settings-block__payment-methods">
|
||||
{ paymentMethods
|
||||
// Remove empty/invalid payment method entries.
|
||||
.filter( ( m ) => m.id )
|
||||
.map( ( paymentMethod ) => (
|
||||
<PaymentMethodItemBlock
|
||||
key={ paymentMethod.id }
|
||||
paymentMethod={ paymentMethod }
|
||||
isSelected={ paymentMethod.enabled }
|
||||
onSelect={ ( checked ) =>
|
||||
handleSelect( paymentMethod.id, checked )
|
||||
}
|
||||
onTriggerModal={ () =>
|
||||
onTriggerModal?.( paymentMethod.id )
|
||||
}
|
||||
/>
|
||||
) ) }
|
||||
</SettingsBlock>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,55 +1,47 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { Content, ContentWrapper } from './Elements';
|
||||
import { Content } from './Elements';
|
||||
|
||||
const SettingsCard = ( {
|
||||
id,
|
||||
className: extraClassName,
|
||||
className,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
contentItems,
|
||||
contentContainer = true,
|
||||
} ) => {
|
||||
const className = classNames( 'ppcp-r-settings-card', extraClassName );
|
||||
|
||||
const renderContent = () => {
|
||||
// If contentItems array is provided, wrap each item in Content component
|
||||
if ( contentItems ) {
|
||||
return (
|
||||
<ContentWrapper>
|
||||
{ contentItems.map( ( item ) => (
|
||||
<Content key={ item.key } id={ item.key }>
|
||||
{ item }
|
||||
</Content>
|
||||
) ) }
|
||||
</ContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise handle regular children with contentContainer prop
|
||||
if ( contentContainer ) {
|
||||
return <Content>{ children }</Content>;
|
||||
}
|
||||
|
||||
return children;
|
||||
const cardClassNames = classNames( 'ppcp-r-settings-card', className );
|
||||
const cardProps = {
|
||||
className: cardClassNames,
|
||||
id,
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={ id } className={ className }>
|
||||
<div { ...cardProps }>
|
||||
<div className="ppcp-r-settings-card__header">
|
||||
<div className="ppcp-r-settings-card__content-inner">
|
||||
<span className="ppcp-r-settings-card__title">
|
||||
{ title }
|
||||
</span>
|
||||
<p className="ppcp-r-settings-card__description">
|
||||
<div className="ppcp-r-settings-card__description">
|
||||
{ description }
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ renderContent() }
|
||||
|
||||
<InnerContent showCards={ contentContainer }>
|
||||
{ children }
|
||||
</InnerContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsCard;
|
||||
|
||||
const InnerContent = ( { showCards, children } ) => {
|
||||
if ( showCards ) {
|
||||
return <Content>{ children }</Content>;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
|
|
@ -9,9 +9,7 @@ const SpinnerOverlay = ( { message = null } ) => {
|
|||
return (
|
||||
<div className="ppcp-r-spinner-overlay">
|
||||
{ message && (
|
||||
<span className="ppcp-r-spinner-overlay__message">
|
||||
{ message }
|
||||
</span>
|
||||
<span className="ppcp--spinner-message">{ message }</span>
|
||||
) }
|
||||
<Spinner />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Temporary component, until the experimental VStack/HStack block editor component is stable.
|
||||
*
|
||||
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-hstack--docs
|
||||
* @see https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-vstack--docs
|
||||
* @file
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
|
||||
const Stack = ( { type, className, spacing, children } ) => {
|
||||
const wrapperClass = classNames(
|
||||
'components-flex',
|
||||
`components-${ type }-stack`,
|
||||
className
|
||||
);
|
||||
|
||||
const styles = {
|
||||
gap: `calc(${ 4 * spacing }px)`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ wrapperClass } style={ styles }>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const HStack = ( { className, spacing = 3, children } ) => {
|
||||
return (
|
||||
<Stack type="h" className={ className } spacing={ spacing }>
|
||||
{ children }
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const VStack = ( { className, spacing = 3, children } ) => {
|
||||
return (
|
||||
<Stack type="v" className={ className } spacing={ spacing }>
|
||||
{ children }
|
||||
</Stack>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue