Typescript conversion of DynamicForm Component (https://github.com/woocommerce/woocommerce-admin/pull/6981)

This commit is contained in:
Joel Thiessen 2021-05-18 12:46:21 -07:00 committed by GitHub
parent f0b494142e
commit 9fda36439e
28 changed files with 370 additions and 306 deletions

View File

@ -37,6 +37,7 @@ module.exports = {
// Making use of typescript no-shadow instead, fixes issues with enum.
'no-shadow': 'off',
'@typescript-eslint/no-shadow': [ 'error' ],
'@typescript-eslint/no-empty-function': 'off',
},
},
],

View File

@ -6,8 +6,8 @@ import interpolateComponents from 'interpolate-components';
import { useDispatch, useSelect } from '@wordpress/data';
import {
Link,
SettingsForm,
WooRemotePaymentSettings,
DynamicForm,
WooRemotePaymentForm,
Spinner,
} from '@woocommerce/components';
import apiFetch from '@wordpress/api-fetch';
@ -29,7 +29,7 @@ export const PaymentConnect = ( {
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { createNotice } = useDispatch( 'core/notices' );
const slot = useSlot( `woocommerce_remote_payment_settings_${ key }` );
const slot = useSlot( `woocommerce_remote_payment_form_${ key }` );
const hasFills = Boolean( slot?.fills?.length );
const [ state, setState ] = useState( 'loading' );
const [ fields, setFields ] = useState( null );
@ -70,6 +70,8 @@ export const PaymentConnect = ( {
} );
const updateSettings = async ( values ) => {
recordConnectStartEvent( key );
const options = {};
fields.forEach( ( field ) => {
@ -107,15 +109,15 @@ export const PaymentConnect = ( {
const getField = ( fieldId ) =>
fields.find( ( field ) => field.id === fieldId );
for ( const [ valueKey, value ] of Object.entries( values ) ) {
const field = getField( valueKey );
for ( const [ fieldKey, value ] of Object.entries( values ) ) {
const field = getField( fieldKey );
// 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 }`;
if ( ! ( value || field.type === 'checkbox' ) ) {
errors[ fieldKey ] = `Please enter your ${ label }`;
}
}
@ -138,13 +140,12 @@ export const PaymentConnect = ( {
},
} );
const DefaultSettings = ( props ) => (
<SettingsForm
const DefaultForm = ( props ) => (
<DynamicForm
fields={ fields }
isBusy={ isOptionsRequesting }
onSubmit={ updateSettings }
onButtonClick={ () => recordConnectStartEvent( key ) }
buttonLabel={ __( 'Proceed', 'woocommerce-admin' ) }
submitLabel={ __( 'Proceed', 'woocommerce-admin' ) }
validate={ validate }
{ ...props }
/>
@ -168,9 +169,9 @@ export const PaymentConnect = ( {
return (
<>
{ hasFills ? (
<WooRemotePaymentSettings.Slot
<WooRemotePaymentForm.Slot
fillProps={ {
defaultSettings: DefaultSettings,
defaultForm: DefaultForm,
defaultSubmit: updateSettings,
defaultFields: fields,
markConfigured: () => markConfigured( key ),
@ -179,7 +180,7 @@ export const PaymentConnect = ( {
/>
) : (
<>
<DefaultSettings />
<DefaultForm />
<p>{ helpText }</p>
</>
) }

View File

@ -120,7 +120,7 @@ export const RemotePayments = ( { query } ) => {
};
const currentMethod = useMemo( () => {
if ( ! methods.length || ! query.method || isResolving ) {
if ( ! query.method || isResolving || ! methods.length ) {
return null;
}

View File

@ -0,0 +1,42 @@
# DynamicForm
A component to handle form state and provide input helper props.
## Usage
```jsx
const initialValues = { firstName: '' };
<DynamicForm
fields={ fields }
onSubmit={ ( values ) => {
setSubmitted( values );
} }
isBusy={ false }
onChange={ () => {} }
validate={ () => ( {} ) }
submitLabel="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 |
| `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 |
| `submitLabel` | 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

View File

@ -0,0 +1,119 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { Form } from '../index';
import {
TextField,
PasswordField,
CheckboxField,
SelectField,
} from './field-types';
import { Field, FormInputProps } from './types';
type DynamicFormProps = {
fields: Field[] | { [ key: string ]: Field };
validate: ( values: Record< string, string > ) => Record< string, string >;
isBusy?: boolean;
onSubmit?: ( values: Record< string, string > ) => void;
onChange?: (
value: Record< string, string >,
values: Record< string, string >[],
result: boolean
) => void;
submitLabel?: string;
};
const fieldTypeMap = {
text: TextField,
password: PasswordField,
checkbox: CheckboxField,
select: SelectField,
default: TextField,
};
export const DynamicForm: React.FC< DynamicFormProps > = ( {
fields: baseFields = [],
isBusy = false,
onSubmit = () => {},
onChange = () => {},
validate = () => ( {} ),
submitLabel = __( '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 = () =>
fields.reduce(
( data, field ) => ( {
...data,
[ field.id ]:
field.type === 'checkbox'
? field.value === 'yes'
: field.value,
} ),
{}
);
return (
<Form
initialValues={ getInitialConfigValues() }
onChangeCallback={ onChange }
onSubmitCallback={ onSubmit }
validate={ validate }
>
{ ( {
getInputProps,
handleSubmit,
}: {
getInputProps: ( name: string ) => FormInputProps;
handleSubmit: () => void;
} ) => {
return (
<div className="woocommerce-component_dynamic-form">
{ fields.map( ( field ) => {
if (
field.type &&
! ( field.type in fieldTypeMap )
) {
/* eslint-disable no-console */
console.warn(
`Field type of ${ field.type } not current supported in DynamicForm component`
);
/* eslint-enable no-console */
return null;
}
const Control =
fieldTypeMap[ field.type || 'default' ];
return (
<Control
key={ field.id }
field={ field }
{ ...getInputProps( field.id ) }
/>
);
} ) }
<Button
isPrimary
isBusy={ isBusy }
onClick={ () => {
handleSubmit();
} }
>
{ submitLabel }
</Button>
</div>
);
} }
</Form>
);
};

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { CheckboxControl } from '@wordpress/components';
/**
* Internal dependencies
*/
import { ControlProps } from '../types';
export const CheckboxField: React.FC< ControlProps > = ( {
field,
onChange,
...props
} ) => {
const { label, description } = field;
return (
<CheckboxControl
onChange={ ( val ) => onChange( val ) }
title={ description }
label={ label }
{ ...props }
/>
);
};

View File

@ -0,0 +1,9 @@
/**
* Internal dependencies
*/
import { TextField } from './field-text';
import { ControlProps } from '../types';
export const PasswordField: React.FC< ControlProps > = ( props ) => {
return <TextField { ...props } type="password" />;
};

View File

@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
*/
import { SelectControl } from '../../index';
import { ControlProps } from '../types';
type SelectControlOption = {
key: string;
label: string;
value: { id: string };
};
const transformOptions = ( options: Record< string, string > ) =>
Object.entries( options ).map( ( [ key, value ] ) => ( {
key,
label: value,
value: { id: key },
} ) );
export const SelectField: React.FC< ControlProps > = ( {
field,
...props
} ) => {
const { description, label, options = {} } = field;
const transformedOptions: SelectControlOption[] = useMemo(
() => transformOptions( options ),
[ options ]
);
return (
<SelectControl
title={ description }
label={ label }
options={ transformedOptions }
{ ...props }
/>
);
};

View File

@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import { TextControl } from '../../index';
import { ControlProps } from '../types';
export const TextField: React.FC< ControlProps & { type?: string } > = ( {
field,
type = 'text',
...props
} ) => {
const { label, description } = field;
return (
<TextControl
type={ type }
title={ description }
label={ label }
{ ...props }
/>
);
};

View File

@ -0,0 +1,4 @@
export * from './field-text';
export * from './field-password';
export * from './field-checkbox';
export * from './field-select';

View File

@ -0,0 +1 @@
export * from './dynamic-form';

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { SettingsForm } from '@woocommerce/components';
import { DynamicForm } from '@woocommerce/components';
import { useState } from '@wordpress/element';
const fields = [
@ -48,8 +48,8 @@ const fields = [
label: 'Checkbox style',
description: 'This is an example checkbox field.',
type: 'checkbox',
value: 'yes',
default: 'yes',
value: 'no',
default: 'no',
tip: 'This is an example checkbox field.',
placeholder: '',
},
@ -64,22 +64,19 @@ const validate = ( values ) => {
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() }`;
if ( ! ( value || field.type === 'checkbox' ) ) {
errors[ key ] = `Please enter your ${ field.label.toLowerCase() }`;
}
}
return errors;
};
const SettingsExample = () => {
const DynamicExample = () => {
const [ submitted, setSubmitted ] = useState( null );
return (
<>
<SettingsForm
<DynamicForm
fields={ fields }
onSubmit={ ( values ) => setSubmitted( values ) }
validate={ validate }
@ -90,9 +87,9 @@ const SettingsExample = () => {
);
};
export const Basic = () => <SettingsExample />;
export const Basic = () => <DynamicExample />;
export default {
title: 'WooCommerce Admin/components/SettingsForm',
component: SettingsForm,
title: 'WooCommerce Admin/components/DynamicForm',
component: DynamicForm,
};

View File

@ -1,4 +1,4 @@
.woocommerce-component-settings {
.woocommerce-component_dynamic-form {
.components-base-control {
margin-top: $gap;
margin-bottom: $gap;

View File

@ -0,0 +1,22 @@
export type Field = {
id: string;
type: 'text' | 'password' | 'checkbox' | 'select';
title: string;
label: string;
description?: string;
default?: string;
class?: string;
css?: string;
options?: Record< string, string >;
tip?: string;
value?: string;
placeholder?: string;
};
export type FormInputProps = React.InputHTMLAttributes< HTMLInputElement > & {
onChange: ( value: string | boolean ) => void;
};
export type ControlProps = FormInputProps & {
field: Field;
};

View File

@ -64,5 +64,5 @@ 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';
export { default as WooRemotePaymentForm } from './woo-remote-payment-form';
export { DynamicForm } from './dynamic-form';

View File

@ -1,44 +0,0 @@
# 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

View File

@ -1,96 +0,0 @@
/**
* 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>
);
};

View File

@ -1,4 +0,0 @@
export { SettingText } from './setting-text';
export { SettingPassword } from './setting-password';
export { SettingCheckbox } from './setting-checkbox';
export { SettingSelect } from './setting-select';

View File

@ -1,17 +0,0 @@
/**
* 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 }
/>
);
};

View File

@ -1,8 +0,0 @@
/**
* Internal dependencies
*/
import { SettingText } from './setting-text';
export const SettingPassword = ( props ) => {
return <SettingText { ...props } type="password" />;
};

View File

@ -1,38 +0,0 @@
/**
* 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 }
/>
);
};

View File

@ -1,18 +0,0 @@
/**
* 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 }
/>
);
};

View File

@ -39,4 +39,4 @@
@import 'view-more-list/style.scss';
@import 'web-preview/style.scss';
@import 'badge/style.scss';
@import 'settings-form/style.scss';
@import 'dynamic-form/style.scss';

View File

@ -0,0 +1,32 @@
# WooRemotePaymentForm Slot & Fill
A Slotfill component that will replace the <DynamicForm /> component involved in displaying the form while adding a gateway via the payment task.
## Usage
```jsx
<WooRemotePaymentForm id={ key }>
{({defaultForm: DefaultForm}) => <p>Fill Content</p>}
</WooRemotePaymentForm>
<WooRemotePaymentForm.Slot id={ key } />
```
### WooRemotePaymentForm (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 |
| ---------------- | --------- | -------------------------------------------------------------------------------------------------------- |
| `defaultForm` | Component | The default instance of the <DynamicForm> 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 |
### WooRemotePaymentForm.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 |

View File

@ -0,0 +1,17 @@
/**
* External dependencies
*/
import { Slot, Fill } from '@wordpress/components';
const WooRemotePaymentForm = ( { id, ...props } ) => (
<Fill name={ 'woocommerce_remote_payment_form_' + id } { ...props } />
);
WooRemotePaymentForm.Slot = ( { id, fillProps } ) => (
<Slot
name={ 'woocommerce_remote_payment_form_' + id }
fillProps={ fillProps }
/>
);
export default WooRemotePaymentForm;

View File

@ -1,32 +0,0 @@
# 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 |

View File

@ -1,17 +0,0 @@
/**
* 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;

View File

@ -76,6 +76,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
== Unreleased ==
- Fix: Autocompleter for custom Search in CompareFilter #6911
- Dev: Converting <SettingsForm /> component to TypeScript. #6981
- 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