From 073a220b59c0a0a0ff3da8a343d91acf4fb9a42b Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Tue, 11 May 2021 09:36:56 -0700 Subject: [PATCH] Adding Slotfill extension components for remote payments (https://github.com/woocommerce/woocommerce-admin/pull/6932) --- .../activity-panel/reviews/test/index.js | 18 +- .../navigation/components/Item/index.js | 2 +- .../components/PaymentConnect.js | 177 +++++++++++------- .../components/PaymentMethod.js | 52 +++-- .../packages/components/src/index.js | 3 + .../components/src/settings-form/README.md | 44 +++++ .../components/src/settings-form/index.js | 96 ++++++++++ .../src/settings-form/stories/index.js | 98 ++++++++++ .../components/src/settings-form/style.scss | 13 ++ .../src/settings-form/types/index.js | 4 + .../settings-form/types/setting-checkbox.js | 17 ++ .../settings-form/types/setting-password.js | 8 + .../src/settings-form/types/setting-select.js | 38 ++++ .../src/settings-form/types/setting-text.js | 18 ++ .../packages/components/src/style.scss | 1 + .../src/woo-remote-payment-settings/README.md | 32 ++++ .../src/woo-remote-payment-settings/index.js | 17 ++ .../src/woo-remote-payment/README.md | 31 +++ .../src/woo-remote-payment/index.js | 14 ++ plugins/woocommerce-admin/readme.txt | 2 +- .../woocommerce-admin/tests/js/util/index.js | 23 +++ 21 files changed, 621 insertions(+), 87 deletions(-) create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/stories/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/style.scss create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-checkbox.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-password.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-select.js create mode 100644 plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-text.js create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/index.js create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment/README.md create mode 100644 plugins/woocommerce-admin/packages/components/src/woo-remote-payment/index.js create mode 100644 plugins/woocommerce-admin/tests/js/util/index.js diff --git a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js index b3559a832a4..d75c3a15cc0 100644 --- a/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js +++ b/plugins/woocommerce-admin/client/homescreen/activity-panel/reviews/test/index.js @@ -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' ) ); diff --git a/plugins/woocommerce-admin/client/navigation/components/Item/index.js b/plugins/woocommerce-admin/client/navigation/components/Item/index.js index fccc608f7a5..ff47b180221 100644 --- a/plugins/woocommerce-admin/client/navigation/components/Item/index.js +++ b/plugins/woocommerce-admin/client/navigation/components/Item/index.js @@ -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', { diff --git a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js index 4d9c6fbc6df..cde2506dcba 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js +++ b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentConnect.js @@ -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 ( -
( + recordConnectStartEvent( key ) } + buttonLabel={ __( 'Proceed', 'woocommerce-admin' ) } validate={ validate } - > - { ( { getInputProps, handleSubmit } ) => { - return ( - <> - { ( fields || [] ).map( ( field ) => ( - - ) ) } + { ...props } + /> + ); - + if ( state === 'error' ) { + return ( + + { + ( __( 'There was an error loading the payment fields' ), + 'woocommerce-admin' ) + } + + ); + } -

{ helpText }

- - ); - } } - + if ( state === 'loading' ) { + return ; + } + + return ( + <> + { hasFills ? ( + markConfigured( key ), + } } + id={ key } + /> + ) : ( + <> + +

{ helpText }

+ + ) } + ); }; diff --git a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js index 3c5eab666b2..ec29eee5782 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js +++ b/plugins/woocommerce-admin/client/task-list/tasks/payments/RemotePayments/components/PaymentMethod.js @@ -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 ) => ( + + ); + return ( - + { hasFills ? ( + + ) : ( + + ) } ); diff --git a/plugins/woocommerce-admin/packages/components/src/index.js b/plugins/woocommerce-admin/packages/components/src/index.js index a7f3d4ca144..b07839d35dd 100644 --- a/plugins/woocommerce-admin/packages/components/src/index.js +++ b/plugins/woocommerce-admin/packages/components/src/index.js @@ -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'; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/README.md b/plugins/woocommerce-admin/packages/components/src/settings-form/README.md new file mode 100644 index 00000000000..ec9a78a529a --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/README.md @@ -0,0 +1,44 @@ +# SettingsForm + +A component to handle form state and provide input helper props. + +## Usage + +```jsx +const initialValues = { firstName: '' }; + + { + 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 diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/index.js b/plugins/woocommerce-admin/packages/components/src/settings-form/index.js new file mode 100644 index 00000000000..da256df3c53 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/index.js @@ -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 ( +
+ { ( { getInputProps, handleSubmit } ) => { + return ( +
+ { 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 ( + + ); + } ) } + + +
+ ); + } } +
+ ); +}; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/stories/index.js b/plugins/woocommerce-admin/packages/components/src/settings-form/stories/index.js new file mode 100644 index 00000000000..ea8a7007698 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/stories/index.js @@ -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 ( + <> + setSubmitted( values ) } + validate={ validate } + /> +

Submitted:

+

{ submitted ? JSON.stringify( submitted, null, 3 ) : 'None' }

+ + ); +}; + +export const Basic = () => ; + +export default { + title: 'WooCommerce Admin/components/SettingsForm', + component: SettingsForm, +}; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/style.scss b/plugins/woocommerce-admin/packages/components/src/settings-form/style.scss new file mode 100644 index 00000000000..df8ad8755cb --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/style.scss @@ -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; + } + } + } +} diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/types/index.js b/plugins/woocommerce-admin/packages/components/src/settings-form/types/index.js new file mode 100644 index 00000000000..8ec30026aff --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/types/index.js @@ -0,0 +1,4 @@ +export { SettingText } from './setting-text'; +export { SettingPassword } from './setting-password'; +export { SettingCheckbox } from './setting-checkbox'; +export { SettingSelect } from './setting-select'; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-checkbox.js b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-checkbox.js new file mode 100644 index 00000000000..73f56e418ba --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-checkbox.js @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { CheckboxControl } from '@wordpress/components'; + +export const SettingCheckbox = ( { field, ...props } ) => { + const { label, id, description } = field; + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-password.js b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-password.js new file mode 100644 index 00000000000..1d5c820cb2e --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-password.js @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { SettingText } from './setting-text'; + +export const SettingPassword = ( props ) => { + return ; +}; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-select.js b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-select.js new file mode 100644 index 00000000000..d7f5531e312 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-select.js @@ -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 ( + + ); +}; diff --git a/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-text.js b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-text.js new file mode 100644 index 00000000000..23c43ac3aaa --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/settings-form/types/setting-text.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { TextControl } from '../../index'; + +export const SettingText = ( { field, type = 'text', ...props } ) => { + const { id, label, description } = field; + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/packages/components/src/style.scss b/plugins/woocommerce-admin/packages/components/src/style.scss index af7df1e4600..1b8a1886c56 100644 --- a/plugins/woocommerce-admin/packages/components/src/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/style.scss @@ -39,3 +39,4 @@ @import 'view-more-list/style.scss'; @import 'web-preview/style.scss'; @import 'badge/style.scss'; +@import 'settings-form/style.scss'; diff --git a/plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/README.md b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/README.md new file mode 100644 index 00000000000..aa9611a1eae --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/README.md @@ -0,0 +1,32 @@ +# WooRemotePaymentSettings Slot & Fill + +A Slotfill component that will replace the component involved in displaying the form while adding a gateway via the payment task. + +## Usage + +```jsx + + {({defaultSettings: DefaultSettings}) =>

Fill Content

} +
+ + +``` + +### 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 component. Any provided props will override the given defaults | +| `defaultSubmit` | Function | The default submit handler that is provided to the
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 | diff --git a/plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/index.js b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/index.js new file mode 100644 index 00000000000..389516bea86 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment-settings/index.js @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { Slot, Fill } from '@wordpress/components'; + +const WooRemotePaymentSettings = ( { id, ...props } ) => ( + +); + +WooRemotePaymentSettings.Slot = ( { id, fillProps } ) => ( + +); + +export default WooRemotePaymentSettings; diff --git a/plugins/woocommerce-admin/packages/components/src/woo-remote-payment/README.md b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment/README.md new file mode 100644 index 00000000000..bb17a656385 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment/README.md @@ -0,0 +1,31 @@ +# WooRemotePayment Slot & Fill + +A Slotfill component that will replace the involved in the installation for a gateway via the payment task. + +## Usage + +```jsx + + {({defaultStepper: DefaultStepper}) =>

Fill Content

} +
+ + +``` + +### 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 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 | diff --git a/plugins/woocommerce-admin/packages/components/src/woo-remote-payment/index.js b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment/index.js new file mode 100644 index 00000000000..acc8f5f15e1 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/woo-remote-payment/index.js @@ -0,0 +1,14 @@ +/** + * External dependencies + */ +import { Slot, Fill } from '@wordpress/components'; + +const WooRemotePayment = ( { id, ...props } ) => ( + +); + +WooRemotePayment.Slot = ( { id, fillProps } ) => ( + +); + +export default WooRemotePayment; diff --git a/plugins/woocommerce-admin/readme.txt b/plugins/woocommerce-admin/readme.txt index 760303731dd..4e433f37522 100644 --- a/plugins/woocommerce-admin/readme.txt +++ b/plugins/woocommerce-admin/readme.txt @@ -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 diff --git a/plugins/woocommerce-admin/tests/js/util/index.js b/plugins/woocommerce-admin/tests/js/util/index.js new file mode 100644 index 00000000000..1972dab47e5 --- /dev/null +++ b/plugins/woocommerce-admin/tests/js/util/index.js @@ -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 );