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:
carmenmaymo 2025-01-28 12:07:25 +01:00
commit bce124aa8d
No known key found for this signature in database
GPG key ID: 6023F686B0F3102E
68 changed files with 1244 additions and 1114 deletions

View file

@ -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>

View file

@ -10,6 +10,7 @@ const ControlTextInput = ( {
} ) => (
<Action>
<TextControl
__nextHasNoMarginBottom={ true }
className="ppcp-r-vertical-text-control"
placeholder={ placeholder }
value={ value }

View file

@ -24,6 +24,7 @@ const Checkbox = ( {
return (
<CheckboxControl
__nextHasNoMarginBottom={ true }
label={ label }
value={ value }
checked={ checked }

View file

@ -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 }
/>
)

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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;
};

View file

@ -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>

View file

@ -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>
);
};