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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ReviewsPanel } from '../';
|
import { ReviewsPanel } from '../';
|
||||||
|
import { getByTextWithMarkup } from '../../../../../tests/js/util';
|
||||||
|
|
||||||
const REVIEW = {
|
const REVIEW = {
|
||||||
id: 10,
|
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.mock( '../checkmark-circle-icon', () =>
|
||||||
jest.fn().mockImplementation( () => '[checkmark-circle-icon]' )
|
jest.fn().mockImplementation( () => '[checkmark-circle-icon]' )
|
||||||
);
|
);
|
||||||
|
@ -66,6 +60,7 @@ describe( 'ReviewsPanel', () => {
|
||||||
isError={ false }
|
isError={ false }
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [] }
|
reviews={ [] }
|
||||||
|
createNotice={ () => {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect( screen.queryByRole( 'section' ) ).toBeNull();
|
expect( screen.queryByRole( 'section' ) ).toBeNull();
|
||||||
|
@ -78,9 +73,11 @@ describe( 'ReviewsPanel', () => {
|
||||||
isError={ false }
|
isError={ false }
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [ REVIEW ] }
|
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', () => {
|
it( 'should render checkmark circle icon in the review title, if review is verfied owner', () => {
|
||||||
|
@ -90,6 +87,7 @@ describe( 'ReviewsPanel', () => {
|
||||||
isError={ false }
|
isError={ false }
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [ { ...REVIEW, verified: true } ] }
|
reviews={ [ { ...REVIEW, verified: true } ] }
|
||||||
|
createNotice={ () => {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const header = screen.getByRole( 'heading', { level: 3 } );
|
const header = screen.getByRole( 'heading', { level: 3 } );
|
||||||
|
@ -104,6 +102,7 @@ describe( 'ReviewsPanel', () => {
|
||||||
isError={ false }
|
isError={ false }
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [ REVIEW ] }
|
reviews={ [ REVIEW ] }
|
||||||
|
createNotice={ () => {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect( screen.queryByText( 'Approve' ) ).toBeInTheDocument();
|
expect( screen.queryByText( 'Approve' ) ).toBeInTheDocument();
|
||||||
|
@ -122,6 +121,7 @@ describe( 'ReviewsPanel', () => {
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [ REVIEW ] }
|
reviews={ [ REVIEW ] }
|
||||||
updateReview={ clickHandler }
|
updateReview={ clickHandler }
|
||||||
|
createNotice={ () => {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
fireEvent.click( screen.getByText( 'Approve' ) );
|
fireEvent.click( screen.getByText( 'Approve' ) );
|
||||||
|
@ -141,6 +141,7 @@ describe( 'ReviewsPanel', () => {
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [ REVIEW ] }
|
reviews={ [ REVIEW ] }
|
||||||
updateReview={ clickHandler }
|
updateReview={ clickHandler }
|
||||||
|
createNotice={ () => {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
fireEvent.click( screen.getByText( 'Mark as spam' ) );
|
fireEvent.click( screen.getByText( 'Mark as spam' ) );
|
||||||
|
@ -160,6 +161,7 @@ describe( 'ReviewsPanel', () => {
|
||||||
isRequesting={ false }
|
isRequesting={ false }
|
||||||
reviews={ [ REVIEW ] }
|
reviews={ [ REVIEW ] }
|
||||||
deleteReview={ clickHandler }
|
deleteReview={ clickHandler }
|
||||||
|
createNotice={ () => {} }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
fireEvent.click( screen.getByText( 'Delete' ) );
|
fireEvent.click( screen.getByText( 'Delete' ) );
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { WooNavigationItem } from '@woocommerce/navigation';
|
||||||
|
|
||||||
const Item = ( { item } ) => {
|
const Item = ( { item } ) => {
|
||||||
const slot = useSlot( 'woocommerce_navigation_' + item.id );
|
const slot = useSlot( 'woocommerce_navigation_' + item.id );
|
||||||
const hasFills = Boolean( slot.fills && slot.fills.length );
|
const hasFills = Boolean( slot?.fills?.length );
|
||||||
|
|
||||||
const trackClick = ( id ) => {
|
const trackClick = ( id ) => {
|
||||||
recordEvent( 'navigation_click', {
|
recordEvent( 'navigation_click', {
|
||||||
|
|
|
@ -2,20 +2,66 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import { Button } from '@wordpress/components';
|
|
||||||
import interpolateComponents from 'interpolate-components';
|
import interpolateComponents from 'interpolate-components';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
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 { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
import { useSlot, Text } from '@woocommerce/experimental';
|
||||||
|
|
||||||
export const PaymentConnect = ( {
|
export const PaymentConnect = ( {
|
||||||
markConfigured,
|
markConfigured,
|
||||||
method,
|
method,
|
||||||
recordConnectStartEvent,
|
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 { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||||
const { createNotice } = useDispatch( 'core/notices' );
|
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 isOptionsRequesting = useSelect( ( select ) => {
|
||||||
const { isOptionsUpdating } = select( OPTIONS_STORE_NAME );
|
const { isOptionsUpdating } = select( OPTIONS_STORE_NAME );
|
||||||
|
@ -23,37 +69,6 @@ export const PaymentConnect = ( {
|
||||||
return isOptionsUpdating();
|
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 updateSettings = async ( values ) => {
|
||||||
const options = {};
|
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( {
|
const helpText = interpolateComponents( {
|
||||||
mixedString: __(
|
mixedString: __(
|
||||||
'Your API details can be obtained from your {{link/}}',
|
'Your API details can be obtained from your {{link/}}',
|
||||||
|
@ -103,39 +138,51 @@ export const PaymentConnect = ( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return (
|
const DefaultSettings = ( props ) => (
|
||||||
<Form
|
<SettingsForm
|
||||||
initialValues={ getInitialConfigValues() }
|
fields={ fields }
|
||||||
onSubmitCallback={ updateSettings }
|
isBusy={ isOptionsRequesting }
|
||||||
|
onSubmit={ updateSettings }
|
||||||
|
onButtonClick={ () => recordConnectStartEvent( key ) }
|
||||||
|
buttonLabel={ __( 'Proceed', 'woocommerce-admin' ) }
|
||||||
validate={ validate }
|
validate={ validate }
|
||||||
>
|
{ ...props }
|
||||||
{ ( { getInputProps, handleSubmit } ) => {
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( state === 'error' ) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{
|
||||||
|
( __( 'There was an error loading the payment fields' ),
|
||||||
|
'woocommerce-admin' )
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( state === 'loading' ) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ ( fields || [] ).map( ( field ) => (
|
{ hasFills ? (
|
||||||
<TextControl
|
<WooRemotePaymentSettings.Slot
|
||||||
key={ field.name }
|
fillProps={ {
|
||||||
label={ field.label }
|
defaultSettings: DefaultSettings,
|
||||||
required
|
defaultSubmit: updateSettings,
|
||||||
{ ...getInputProps( field.name ) }
|
defaultFields: fields,
|
||||||
/>
|
markConfigured: () => markConfigured( key ),
|
||||||
) ) }
|
|
||||||
|
|
||||||
<Button
|
|
||||||
isPrimary
|
|
||||||
isBusy={ isOptionsRequesting }
|
|
||||||
onClick={ ( event ) => {
|
|
||||||
recordConnectStartEvent( key );
|
|
||||||
handleSubmit( event );
|
|
||||||
} }
|
} }
|
||||||
>
|
id={ key }
|
||||||
{ __( 'Proceed', 'woocommerce-admin' ) }
|
/>
|
||||||
</Button>
|
) : (
|
||||||
|
<>
|
||||||
|
<DefaultSettings />
|
||||||
<p>{ helpText }</p>
|
<p>{ helpText }</p>
|
||||||
</>
|
</>
|
||||||
);
|
) }
|
||||||
} }
|
</>
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,9 +9,11 @@ import {
|
||||||
PLUGINS_STORE_NAME,
|
PLUGINS_STORE_NAME,
|
||||||
pluginNames,
|
pluginNames,
|
||||||
} from '@woocommerce/data';
|
} from '@woocommerce/data';
|
||||||
import { Plugins, Stepper } from '@woocommerce/components';
|
import { Plugins, Stepper, WooRemotePayment } from '@woocommerce/components';
|
||||||
|
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useSlot } from '@woocommerce/experimental';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -24,7 +26,15 @@ export const PaymentMethod = ( {
|
||||||
method,
|
method,
|
||||||
recordConnectStartEvent,
|
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( () => {
|
useEffect( () => {
|
||||||
recordEvent( 'payments_task_stepper_view', {
|
recordEvent( 'payments_task_stepper_view', {
|
||||||
payment_method: key,
|
payment_method: key,
|
||||||
|
@ -71,6 +81,12 @@ export const PaymentMethod = ( {
|
||||||
recordEvent( 'tasklist_payment_install_method', {
|
recordEvent( 'tasklist_payment_install_method', {
|
||||||
plugins,
|
plugins,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
if ( postInstallScript ) {
|
||||||
|
const script = document.createElement( 'script' );
|
||||||
|
script.src = postInstallScript;
|
||||||
|
document.body.append( script );
|
||||||
|
}
|
||||||
} }
|
} }
|
||||||
onError={ ( errors, response ) =>
|
onError={ ( errors, response ) =>
|
||||||
createNoticesFromResponse( response )
|
createNoticesFromResponse( response )
|
||||||
|
@ -102,19 +118,31 @@ export const PaymentMethod = ( {
|
||||||
};
|
};
|
||||||
}, [ title ] );
|
}, [ title ] );
|
||||||
|
|
||||||
|
const DefaultStepper = ( props ) => (
|
||||||
|
<Stepper
|
||||||
|
isVertical
|
||||||
|
isPending={ ! installStep.isComplete || isOptionsRequesting }
|
||||||
|
currentStep={ installStep.isComplete ? 'connect' : 'install' }
|
||||||
|
steps={ [ installStep, connectStep ] }
|
||||||
|
{ ...props }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="woocommerce-task-payment-method woocommerce-task-card">
|
<Card className="woocommerce-task-payment-method woocommerce-task-card">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Stepper
|
{ hasFills ? (
|
||||||
isVertical
|
<WooRemotePayment.Slot
|
||||||
isPending={
|
fillProps={ {
|
||||||
! installStep.isComplete || isOptionsRequesting
|
defaultStepper: DefaultStepper,
|
||||||
}
|
defaultInstallStep: installStep,
|
||||||
currentStep={
|
defaultConnectStep: connectStep,
|
||||||
installStep.isComplete ? 'connect' : 'install'
|
} }
|
||||||
}
|
id={ key }
|
||||||
steps={ [ installStep, connectStep ] }
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<DefaultStepper />
|
||||||
|
) }
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</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 ViewMoreList } from './view-more-list';
|
||||||
export { default as WebPreview } from './web-preview';
|
export { default as WebPreview } from './web-preview';
|
||||||
export { Badge } from './badge';
|
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 'view-more-list/style.scss';
|
||||||
@import 'web-preview/style.scss';
|
@import 'web-preview/style.scss';
|
||||||
@import 'badge/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 ==
|
== Changelog ==
|
||||||
|
|
||||||
== Unreleased ==
|
== Unreleased ==
|
||||||
|
- Enhancement: Adding Slotfills for remote payments and SettingsForm component. #6932
|
||||||
- Fix: Make `Search` accept synchronous `autocompleter.options`. #6884
|
- Fix: Make `Search` accept synchronous `autocompleter.options`. #6884
|
||||||
- Add: Consume remote payment methods on frontend #6867
|
- Add: Consume remote payment methods on frontend #6867
|
||||||
- Add: Add plugin installer to allow installation of plugins via URL #6805
|
- 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