Adding Slotfill extension components for remote payments (https://github.com/woocommerce/woocommerce-admin/pull/6932)
This commit is contained in:
parent
c904690cac
commit
073a220b59
|
@ -7,6 +7,7 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ReviewsPanel } from '../';
|
||||
import { getByTextWithMarkup } from '../../../../../tests/js/util';
|
||||
|
||||
const REVIEW = {
|
||||
id: 10,
|
||||
|
@ -47,13 +48,6 @@ const REVIEW = {
|
|||
},
|
||||
};
|
||||
|
||||
jest.mock( '@woocommerce/components', () => ( {
|
||||
...jest.requireActual( '@woocommerce/components' ),
|
||||
Link: ( { children } ) => {
|
||||
return <>{ children }</>;
|
||||
},
|
||||
} ) );
|
||||
|
||||
jest.mock( '../checkmark-circle-icon', () =>
|
||||
jest.fn().mockImplementation( () => '[checkmark-circle-icon]' )
|
||||
);
|
||||
|
@ -66,6 +60,7 @@ describe( 'ReviewsPanel', () => {
|
|||
isError={ false }
|
||||
isRequesting={ false }
|
||||
reviews={ [] }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
expect( screen.queryByRole( 'section' ) ).toBeNull();
|
||||
|
@ -78,9 +73,11 @@ describe( 'ReviewsPanel', () => {
|
|||
isError={ false }
|
||||
isRequesting={ false }
|
||||
reviews={ [ REVIEW ] }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
expect( screen.getByText( 'Reviewer reviewed Cap' ) ).not.toBeNull();
|
||||
|
||||
expect( getByTextWithMarkup( 'Reviewer reviewed Cap' ) ).not.toBeNull();
|
||||
} );
|
||||
|
||||
it( 'should render checkmark circle icon in the review title, if review is verfied owner', () => {
|
||||
|
@ -90,6 +87,7 @@ describe( 'ReviewsPanel', () => {
|
|||
isError={ false }
|
||||
isRequesting={ false }
|
||||
reviews={ [ { ...REVIEW, verified: true } ] }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
const header = screen.getByRole( 'heading', { level: 3 } );
|
||||
|
@ -104,6 +102,7 @@ describe( 'ReviewsPanel', () => {
|
|||
isError={ false }
|
||||
isRequesting={ false }
|
||||
reviews={ [ REVIEW ] }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
expect( screen.queryByText( 'Approve' ) ).toBeInTheDocument();
|
||||
|
@ -122,6 +121,7 @@ describe( 'ReviewsPanel', () => {
|
|||
isRequesting={ false }
|
||||
reviews={ [ REVIEW ] }
|
||||
updateReview={ clickHandler }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
fireEvent.click( screen.getByText( 'Approve' ) );
|
||||
|
@ -141,6 +141,7 @@ describe( 'ReviewsPanel', () => {
|
|||
isRequesting={ false }
|
||||
reviews={ [ REVIEW ] }
|
||||
updateReview={ clickHandler }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
fireEvent.click( screen.getByText( 'Mark as spam' ) );
|
||||
|
@ -160,6 +161,7 @@ describe( 'ReviewsPanel', () => {
|
|||
isRequesting={ false }
|
||||
reviews={ [ REVIEW ] }
|
||||
deleteReview={ clickHandler }
|
||||
createNotice={ () => {} }
|
||||
/>
|
||||
);
|
||||
fireEvent.click( screen.getByText( 'Delete' ) );
|
||||
|
|
|
@ -7,7 +7,7 @@ import { WooNavigationItem } from '@woocommerce/navigation';
|
|||
|
||||
const Item = ( { item } ) => {
|
||||
const slot = useSlot( 'woocommerce_navigation_' + item.id );
|
||||
const hasFills = Boolean( slot.fills && slot.fills.length );
|
||||
const hasFills = Boolean( slot?.fills?.length );
|
||||
|
||||
const trackClick = ( id ) => {
|
||||
recordEvent( 'navigation_click', {
|
||||
|
|
|
@ -2,20 +2,66 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Button } from '@wordpress/components';
|
||||
import interpolateComponents from 'interpolate-components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { Form, Link, TextControl } from '@woocommerce/components';
|
||||
import {
|
||||
Link,
|
||||
SettingsForm,
|
||||
WooRemotePaymentSettings,
|
||||
Spinner,
|
||||
} from '@woocommerce/components';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { useSlot, Text } from '@woocommerce/experimental';
|
||||
|
||||
export const PaymentConnect = ( {
|
||||
markConfigured,
|
||||
method,
|
||||
recordConnectStartEvent,
|
||||
} ) => {
|
||||
const { api_details_url: apiDetailsUrl, fields, key, title } = method;
|
||||
const {
|
||||
api_details_url: apiDetailsUrl,
|
||||
fields: fieldsConfig,
|
||||
key,
|
||||
title,
|
||||
} = method;
|
||||
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const slot = useSlot( `woocommerce_remote_payment_settings_${ key }` );
|
||||
const hasFills = Boolean( slot?.fills?.length );
|
||||
const [ state, setState ] = useState( 'loading' );
|
||||
const [ fields, setFields ] = useState( null );
|
||||
|
||||
// This transform will be obsolete when we can derive essential fields from the API
|
||||
const settingsTransform = ( settings ) => {
|
||||
const essentialFields = fieldsConfig.map( ( field ) => field.name );
|
||||
|
||||
return Object.values( settings ).filter( ( setting ) =>
|
||||
essentialFields.includes( setting.id )
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: Will soon be replaced with the payments data store implemented in #6918
|
||||
useEffect( () => {
|
||||
apiFetch( {
|
||||
path: `/wc/v3/payment_gateways/${ key }/`,
|
||||
} )
|
||||
.then( ( results ) => {
|
||||
setFields( settingsTransform( results.settings ) );
|
||||
setState( 'loaded' );
|
||||
} )
|
||||
.catch( ( e ) => {
|
||||
setState( 'error' );
|
||||
/* eslint-disable no-console */
|
||||
console.error(
|
||||
`Error fetching information for payment gateway ${ key }`,
|
||||
e.message
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
} );
|
||||
}, [] );
|
||||
|
||||
const isOptionsRequesting = useSelect( ( select ) => {
|
||||
const { isOptionsUpdating } = select( OPTIONS_STORE_NAME );
|
||||
|
@ -23,37 +69,6 @@ export const PaymentConnect = ( {
|
|||
return isOptionsUpdating();
|
||||
} );
|
||||
|
||||
const getInitialConfigValues = () => {
|
||||
if ( fields ) {
|
||||
return fields.reduce( ( data, field ) => {
|
||||
return {
|
||||
...data,
|
||||
[ field.name ]: '',
|
||||
};
|
||||
}, {} );
|
||||
}
|
||||
};
|
||||
|
||||
const validate = ( values ) => {
|
||||
if ( fields ) {
|
||||
return fields.reduce( ( errors, field ) => {
|
||||
if ( ! values[ field.name ] ) {
|
||||
// Matches any word that is capitalized aside from abrevitions like ID.
|
||||
const label = field.label.replace(
|
||||
/([A-Z][a-z]+)/,
|
||||
( val ) => val.toLowerCase()
|
||||
);
|
||||
return {
|
||||
...errors,
|
||||
[ field.name ]: __( 'Please enter your ' ) + label,
|
||||
};
|
||||
}
|
||||
return errors;
|
||||
}, {} );
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const updateSettings = async ( values ) => {
|
||||
const options = {};
|
||||
|
||||
|
@ -87,6 +102,26 @@ export const PaymentConnect = ( {
|
|||
}
|
||||
};
|
||||
|
||||
const validate = ( values ) => {
|
||||
const errors = {};
|
||||
const getField = ( fieldId ) =>
|
||||
fields.find( ( field ) => field.id === fieldId );
|
||||
|
||||
for ( const [ valueKey, value ] of Object.entries( values ) ) {
|
||||
const field = getField( valueKey );
|
||||
// Matches any word that is capitalized aside from abrevitions like ID.
|
||||
const label = field.label.replace( /([A-Z][a-z]+)/g, ( val ) =>
|
||||
val.toLowerCase()
|
||||
);
|
||||
|
||||
if ( ! value ) {
|
||||
errors[ valueKey ] = `Please enter your ${ label }`;
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const helpText = interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Your API details can be obtained from your {{link/}}',
|
||||
|
@ -103,39 +138,51 @@ export const PaymentConnect = ( {
|
|||
},
|
||||
} );
|
||||
|
||||
return (
|
||||
<Form
|
||||
initialValues={ getInitialConfigValues() }
|
||||
onSubmitCallback={ updateSettings }
|
||||
const DefaultSettings = ( props ) => (
|
||||
<SettingsForm
|
||||
fields={ fields }
|
||||
isBusy={ isOptionsRequesting }
|
||||
onSubmit={ updateSettings }
|
||||
onButtonClick={ () => recordConnectStartEvent( key ) }
|
||||
buttonLabel={ __( 'Proceed', 'woocommerce-admin' ) }
|
||||
validate={ validate }
|
||||
>
|
||||
{ ( { getInputProps, handleSubmit } ) => {
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
|
||||
if ( state === 'error' ) {
|
||||
return (
|
||||
<Text>
|
||||
{
|
||||
( __( 'There was an error loading the payment fields' ),
|
||||
'woocommerce-admin' )
|
||||
}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if ( state === 'loading' ) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ ( fields || [] ).map( ( field ) => (
|
||||
<TextControl
|
||||
key={ field.name }
|
||||
label={ field.label }
|
||||
required
|
||||
{ ...getInputProps( field.name ) }
|
||||
/>
|
||||
) ) }
|
||||
|
||||
<Button
|
||||
isPrimary
|
||||
isBusy={ isOptionsRequesting }
|
||||
onClick={ ( event ) => {
|
||||
recordConnectStartEvent( key );
|
||||
handleSubmit( event );
|
||||
{ hasFills ? (
|
||||
<WooRemotePaymentSettings.Slot
|
||||
fillProps={ {
|
||||
defaultSettings: DefaultSettings,
|
||||
defaultSubmit: updateSettings,
|
||||
defaultFields: fields,
|
||||
markConfigured: () => markConfigured( key ),
|
||||
} }
|
||||
>
|
||||
{ __( 'Proceed', 'woocommerce-admin' ) }
|
||||
</Button>
|
||||
|
||||
id={ key }
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<DefaultSettings />
|
||||
<p>{ helpText }</p>
|
||||
</>
|
||||
);
|
||||
} }
|
||||
</Form>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,9 +9,11 @@ import {
|
|||
PLUGINS_STORE_NAME,
|
||||
pluginNames,
|
||||
} from '@woocommerce/data';
|
||||
import { Plugins, Stepper } from '@woocommerce/components';
|
||||
import { Plugins, Stepper, WooRemotePayment } from '@woocommerce/components';
|
||||
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useSlot } from '@woocommerce/experimental';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -24,7 +26,15 @@ export const PaymentMethod = ( {
|
|||
method,
|
||||
recordConnectStartEvent,
|
||||
} ) => {
|
||||
const { key, plugins, title } = method;
|
||||
const {
|
||||
key,
|
||||
plugins,
|
||||
title,
|
||||
post_install_script: postInstallScript,
|
||||
} = method;
|
||||
const slot = useSlot( `woocommerce_remote_payment_${ key }` );
|
||||
const hasFills = Boolean( slot?.fills?.length );
|
||||
|
||||
useEffect( () => {
|
||||
recordEvent( 'payments_task_stepper_view', {
|
||||
payment_method: key,
|
||||
|
@ -71,6 +81,12 @@ export const PaymentMethod = ( {
|
|||
recordEvent( 'tasklist_payment_install_method', {
|
||||
plugins,
|
||||
} );
|
||||
|
||||
if ( postInstallScript ) {
|
||||
const script = document.createElement( 'script' );
|
||||
script.src = postInstallScript;
|
||||
document.body.append( script );
|
||||
}
|
||||
} }
|
||||
onError={ ( errors, response ) =>
|
||||
createNoticesFromResponse( response )
|
||||
|
@ -102,19 +118,31 @@ export const PaymentMethod = ( {
|
|||
};
|
||||
}, [ title ] );
|
||||
|
||||
const DefaultStepper = ( props ) => (
|
||||
<Stepper
|
||||
isVertical
|
||||
isPending={ ! installStep.isComplete || isOptionsRequesting }
|
||||
currentStep={ installStep.isComplete ? 'connect' : 'install' }
|
||||
steps={ [ installStep, connectStep ] }
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-task-payment-method woocommerce-task-card">
|
||||
<CardBody>
|
||||
<Stepper
|
||||
isVertical
|
||||
isPending={
|
||||
! installStep.isComplete || isOptionsRequesting
|
||||
}
|
||||
currentStep={
|
||||
installStep.isComplete ? 'connect' : 'install'
|
||||
}
|
||||
steps={ [ installStep, connectStep ] }
|
||||
{ hasFills ? (
|
||||
<WooRemotePayment.Slot
|
||||
fillProps={ {
|
||||
defaultStepper: DefaultStepper,
|
||||
defaultInstallStep: installStep,
|
||||
defaultConnectStep: connectStep,
|
||||
} }
|
||||
id={ key }
|
||||
/>
|
||||
) : (
|
||||
<DefaultStepper />
|
||||
) }
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -63,3 +63,6 @@ export { default as useFilters } from './higher-order/use-filters';
|
|||
export { default as ViewMoreList } from './view-more-list';
|
||||
export { default as WebPreview } from './web-preview';
|
||||
export { Badge } from './badge';
|
||||
export { default as WooRemotePayment } from './woo-remote-payment';
|
||||
export { default as WooRemotePaymentSettings } from './woo-remote-payment-settings';
|
||||
export { SettingsForm } from './settings-form';
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# SettingsForm
|
||||
|
||||
A component to handle form state and provide input helper props.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
const initialValues = { firstName: '' };
|
||||
|
||||
<SettingsForm
|
||||
fields={ fields }
|
||||
onSubmit={ ( values ) => {
|
||||
setSubmitted( values );
|
||||
} }
|
||||
isBusy={ false }
|
||||
onButtonClick={ () => {} }
|
||||
onChange={ () => {} }
|
||||
validate={ () => ( {} ) }
|
||||
buttonLabel="Submit"
|
||||
/>;
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `fields` | {} or [] | [] | An object to describe the structure and types of all fields, matching the structure returned by the [Settings API](https://docs.woocommerce.com/document/settings-api/) |
|
||||
| `isBusy` | Boolean | false | Boolean indicating busy state of submit button |
|
||||
| `onButtonClick` | Function | `noop` | Callback function executed when submit button is clicked (in addition to form submission) |
|
||||
| `onSubmit` | Function | `noop` | Function to call when a form is submitted with valid fields |
|
||||
| `onChange` | Function | `noop` | Function to call when any values on the form are changed |
|
||||
| `validate` | Function | `noop` | A function that is passed a list of all values and should return an `errors` object with error response |
|
||||
| `buttonLabel` | String | "Proceed" | Label for submit button. |
|
||||
|
||||
### Fields structure
|
||||
|
||||
Please reference the [WordPress settings API documentation](https://docs.woocommerce.com/document/settings-api/) to better understand the structure expected for the fields property. This component accepts the object returned via the `settings` property when querying a gateway via the API, or simply the array provided by `Object.values(settings)`.
|
||||
|
||||
### Currently Supported Types
|
||||
|
||||
- Text
|
||||
- Password
|
||||
- Checkbox
|
||||
- Select
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Form } from '../index';
|
||||
import {
|
||||
SettingText,
|
||||
SettingPassword,
|
||||
SettingCheckbox,
|
||||
SettingSelect,
|
||||
} from './types';
|
||||
|
||||
const typeMap = {
|
||||
text: SettingText,
|
||||
password: SettingPassword,
|
||||
checkbox: SettingCheckbox,
|
||||
select: SettingSelect,
|
||||
default: SettingText,
|
||||
};
|
||||
|
||||
export const SettingsForm = ( {
|
||||
fields: baseFields = [],
|
||||
isBusy = false,
|
||||
onSubmit = () => {},
|
||||
onButtonClick = () => {},
|
||||
onChange = () => {},
|
||||
validate = () => ( {} ),
|
||||
buttonLabel = __( 'Proceed', 'woocommerce-admin' ),
|
||||
} ) => {
|
||||
// Support accepting fields in the format provided by the API (object), but transform to Array
|
||||
const fields =
|
||||
baseFields instanceof Array ? baseFields : Object.values( baseFields );
|
||||
|
||||
const getInitialConfigValues = () => {
|
||||
if ( fields ) {
|
||||
return fields.reduce(
|
||||
( data, field ) => ( {
|
||||
...data,
|
||||
[ field.id ]: field.value,
|
||||
} ),
|
||||
{}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
initialValues={ getInitialConfigValues() }
|
||||
onChangeCallback={ onChange }
|
||||
onSubmitCallback={ onSubmit }
|
||||
validate={ validate }
|
||||
>
|
||||
{ ( { getInputProps, handleSubmit } ) => {
|
||||
return (
|
||||
<div className="woocommerce-component-settings">
|
||||
{ fields.map( ( field ) => {
|
||||
if ( field.type && ! ( field.type in typeMap ) ) {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(
|
||||
`Field type of ${ field.type } not current supported in SettingsForm component`
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
return null;
|
||||
}
|
||||
|
||||
const Control = typeMap[ field.type || 'default' ];
|
||||
return (
|
||||
<Control
|
||||
key={ field.id }
|
||||
field={ field }
|
||||
{ ...getInputProps( field.id ) }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
|
||||
<Button
|
||||
isPrimary
|
||||
isBusy={ isBusy }
|
||||
onClick={ ( event ) => {
|
||||
handleSubmit( event );
|
||||
onButtonClick();
|
||||
} }
|
||||
>
|
||||
{ buttonLabel }
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} }
|
||||
</Form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { SettingsForm } from '@woocommerce/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
id: 'user_name',
|
||||
label: 'Username',
|
||||
description: 'This is your username.',
|
||||
type: 'text',
|
||||
value: '',
|
||||
default: '',
|
||||
tip: 'This is your username.',
|
||||
placeholder: '',
|
||||
},
|
||||
{
|
||||
id: 'pass_phrase',
|
||||
label: 'Passphrase',
|
||||
description:
|
||||
'* Required. Needed to ensure the data passed through is secure.',
|
||||
type: 'password',
|
||||
value: '',
|
||||
default: '',
|
||||
tip: '* Required. Needed to ensure the data passed through is secure.',
|
||||
placeholder: '',
|
||||
},
|
||||
{
|
||||
id: 'button_type',
|
||||
label: 'Button Type',
|
||||
description: 'Select the button type you would like to show.',
|
||||
type: 'select',
|
||||
value: 'buy',
|
||||
default: 'buy',
|
||||
tip: 'Select the button type you would like to show.',
|
||||
placeholder: '',
|
||||
options: {
|
||||
default: 'Default',
|
||||
buy: 'Buy',
|
||||
donate: 'Donate',
|
||||
branded: 'Branded',
|
||||
custom: 'Custom',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'checkbox_sample',
|
||||
label: 'Checkbox style',
|
||||
description: 'This is an example checkbox field.',
|
||||
type: 'checkbox',
|
||||
value: 'yes',
|
||||
default: 'yes',
|
||||
tip: 'This is an example checkbox field.',
|
||||
placeholder: '',
|
||||
},
|
||||
];
|
||||
|
||||
const getField = ( fieldId ) =>
|
||||
fields.find( ( field ) => field.id === fieldId );
|
||||
|
||||
const validate = ( values ) => {
|
||||
const errors = {};
|
||||
|
||||
for ( const [ key, value ] of Object.entries( values ) ) {
|
||||
const field = getField( key );
|
||||
|
||||
if ( ! value ) {
|
||||
errors[ key ] =
|
||||
field.type === 'checkbox'
|
||||
? 'This is required'
|
||||
: `Please enter your ${ field.label.toLowerCase() }`;
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
const SettingsExample = () => {
|
||||
const [ submitted, setSubmitted ] = useState( null );
|
||||
return (
|
||||
<>
|
||||
<SettingsForm
|
||||
fields={ fields }
|
||||
onSubmit={ ( values ) => setSubmitted( values ) }
|
||||
validate={ validate }
|
||||
/>
|
||||
<h4>Submitted:</h4>
|
||||
<p>{ submitted ? JSON.stringify( submitted, null, 3 ) : 'None' }</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Basic = () => <SettingsExample />;
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Admin/components/SettingsForm',
|
||||
component: SettingsForm,
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
.woocommerce-component-settings {
|
||||
.components-base-control {
|
||||
margin-top: $gap;
|
||||
margin-bottom: $gap;
|
||||
position: relative;
|
||||
|
||||
&.has-error {
|
||||
.components-base-control__help {
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export { SettingText } from './setting-text';
|
||||
export { SettingPassword } from './setting-password';
|
||||
export { SettingCheckbox } from './setting-checkbox';
|
||||
export { SettingSelect } from './setting-select';
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CheckboxControl } from '@wordpress/components';
|
||||
|
||||
export const SettingCheckbox = ( { field, ...props } ) => {
|
||||
const { label, id, description } = field;
|
||||
|
||||
return (
|
||||
<CheckboxControl
|
||||
title={ description }
|
||||
key={ id }
|
||||
label={ label }
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SettingText } from './setting-text';
|
||||
|
||||
export const SettingPassword = ( props ) => {
|
||||
return <SettingText { ...props } type="password" />;
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SelectControl } from '../../index';
|
||||
|
||||
const transformOptions = ( options ) => {
|
||||
return Object.keys( options ).reduce( ( all, curr ) => {
|
||||
all.push( {
|
||||
key: curr,
|
||||
label: options[ curr ],
|
||||
value: { id: curr },
|
||||
} );
|
||||
return all;
|
||||
}, [] );
|
||||
};
|
||||
|
||||
export const SettingSelect = ( { field, ...props } ) => {
|
||||
const { description, id, label, options = {} } = field;
|
||||
|
||||
const transformedOptions = useMemo( () => transformOptions( options ), [
|
||||
options,
|
||||
] );
|
||||
|
||||
return (
|
||||
<SelectControl
|
||||
title={ description }
|
||||
label={ label }
|
||||
key={ id }
|
||||
options={ transformedOptions }
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { TextControl } from '../../index';
|
||||
|
||||
export const SettingText = ( { field, type = 'text', ...props } ) => {
|
||||
const { id, label, description } = field;
|
||||
|
||||
return (
|
||||
<TextControl
|
||||
type={ type }
|
||||
title={ description }
|
||||
key={ id }
|
||||
label={ label }
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -39,3 +39,4 @@
|
|||
@import 'view-more-list/style.scss';
|
||||
@import 'web-preview/style.scss';
|
||||
@import 'badge/style.scss';
|
||||
@import 'settings-form/style.scss';
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# WooRemotePaymentSettings Slot & Fill
|
||||
|
||||
A Slotfill component that will replace the <Settings /> component involved in displaying the form while adding a gateway via the payment task.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
<WooRemotePaymentSettings id={ key }>
|
||||
{({defaultSettings: DefaultSettings}) => <p>Fill Content</p>}
|
||||
</WooRemotePaymentSettings>
|
||||
|
||||
<WooRemotePaymentSettings.Slot id={ key } />
|
||||
```
|
||||
|
||||
### WooRemotePaymentSettings (fill)
|
||||
|
||||
This is the fill component. You must provide the `id` prop to identify the slot that this will occupy. If you provide a function as the child of your fill (as shown above), you will receive some helper props to assist in creating your fill:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------------- | --------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `defaultSettings` | Component | The default instance of the <SettingsForm> component. Any provided props will override the given defaults |
|
||||
| `defaultSubmit` | Function | The default submit handler that is provided to the <Form> component |
|
||||
| `defaultFields` | Array | An array of the field configuration objects provided by the API |
|
||||
| `markConfigured` | Function | A helper function that will mark your gateway as configured |
|
||||
|
||||
### WooRemotePaymentSettings.Slot (slot)
|
||||
|
||||
This is the slot component, and will not be used as frequently. It must also receive the required `id` prop that will be identical to the fill `id`.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------ | ---------------------------------------------------------------------------------- |
|
||||
| `fillProps` | Object | The props that will be provided to the fills, by default these are described above |
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
|
||||
const WooRemotePaymentSettings = ( { id, ...props } ) => (
|
||||
<Fill name={ 'woocommerce_remote_payment_settings_' + id } { ...props } />
|
||||
);
|
||||
|
||||
WooRemotePaymentSettings.Slot = ( { id, fillProps } ) => (
|
||||
<Slot
|
||||
name={ 'woocommerce_remote_payment_settings_' + id }
|
||||
fillProps={ fillProps }
|
||||
/>
|
||||
);
|
||||
|
||||
export default WooRemotePaymentSettings;
|
|
@ -0,0 +1,31 @@
|
|||
# WooRemotePayment Slot & Fill
|
||||
|
||||
A Slotfill component that will replace the <Stepper /> involved in the installation for a gateway via the payment task.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
<WooRemotePayment id={ key }>
|
||||
{({defaultStepper: DefaultStepper}) => <p>Fill Content</p>}
|
||||
</WooRemotePayment>
|
||||
|
||||
<WooRemotePayment.Slot id={ key } />
|
||||
```
|
||||
|
||||
### WooRemotePayment (fill)
|
||||
|
||||
This is the fill component. You must provide the `id` prop to identify the slot that this will occupy. If you provide a function as the child of your fill (as shown above), you will receive some helper props to assist in creating your fill:
|
||||
|
||||
| Name | Type | Description |
|
||||
| -------------------- | --------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `defaultStepper` | Component | The default instance of the <Stepper> component. Any provided props will override the given defaults |
|
||||
| `defaultInstallStep` | Object | The object that describes the default step configuration for installation of the gateway |
|
||||
| `defaultConnectStep` | Object | The object that describes the default step configuration for configuration of the gateway |
|
||||
|
||||
### WooRemotePayment.Slot (slot)
|
||||
|
||||
This is the slot component, and will not be used as frequently. It must also receive the required `id` prop that will be identical to the fill `id`.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------- | ------ | ---------------------------------------------------------------------------------- |
|
||||
| `fillProps` | Object | The props that will be provided to the fills, by default these are described above |
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
|
||||
const WooRemotePayment = ( { id, ...props } ) => (
|
||||
<Fill name={ 'woocommerce_remote_payment_' + id } { ...props } />
|
||||
);
|
||||
|
||||
WooRemotePayment.Slot = ( { id, fillProps } ) => (
|
||||
<Slot name={ 'woocommerce_remote_payment_' + id } fillProps={ fillProps } />
|
||||
);
|
||||
|
||||
export default WooRemotePayment;
|
|
@ -74,7 +74,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
|
|||
== Changelog ==
|
||||
|
||||
== Unreleased ==
|
||||
|
||||
- Enhancement: Adding Slotfills for remote payments and SettingsForm component. #6932
|
||||
- Fix: Make `Search` accept synchronous `autocompleter.options`. #6884
|
||||
- Add: Consume remote payment methods on frontend #6867
|
||||
- Add: Add plugin installer to allow installation of plugins via URL #6805
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* For use with react-testing-library, like getText but allows the text to reside in multiple elements
|
||||
*
|
||||
* @param {Object} query - Original query.
|
||||
*
|
||||
* @return {Array} - Array of two arrays, first including truthy values, and second including falsy.
|
||||
*/
|
||||
const withMarkup = ( query ) => ( text ) =>
|
||||
query( ( content, node ) => {
|
||||
const hasText = ( domNode ) => domNode.textContent === text;
|
||||
const childrenDontHaveText = Array.from( node.children ).every(
|
||||
( child ) => ! hasText( child )
|
||||
);
|
||||
|
||||
return hasText( node ) && childrenDontHaveText;
|
||||
} );
|
||||
|
||||
export const getByTextWithMarkup = withMarkup( screen.getByText );
|
Loading…
Reference in New Issue