Adding WooProductFieldItem slotfill (#36315)
This commit is contained in:
parent
c6e1fb767e
commit
f429b9444c
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Adding WooProductFieldItem slotfill.
|
|
@ -84,3 +84,5 @@ export { DynamicForm } from './dynamic-form';
|
||||||
export { default as TourKit } from './tour-kit';
|
export { default as TourKit } from './tour-kit';
|
||||||
export * as TourKitTypes from './tour-kit/types';
|
export * as TourKitTypes from './tour-kit/types';
|
||||||
export { CollapsibleContent } from './collapsible-content';
|
export { CollapsibleContent } from './collapsible-content';
|
||||||
|
export { createOrderedChildren, sortFillsByOrder } from './utils';
|
||||||
|
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import React, { isValidElement, Fragment } from 'react';
|
||||||
|
import { Slot, Fill } from '@wordpress/components';
|
||||||
|
import { cloneElement, createElement } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordered fill item.
|
||||||
|
*
|
||||||
|
* @param {Node} children - Node children.
|
||||||
|
* @param {number} order - Node order.
|
||||||
|
* @param {Array} props - Fill props.
|
||||||
|
* @return {Node} Node.
|
||||||
|
*/
|
||||||
|
function createOrderedChildren< T = Fill.Props >(
|
||||||
|
children: React.ReactNode,
|
||||||
|
order: number,
|
||||||
|
props: T
|
||||||
|
) {
|
||||||
|
if ( typeof children === 'function' ) {
|
||||||
|
return cloneElement( children( props ), { order } );
|
||||||
|
} else if ( isValidElement( children ) ) {
|
||||||
|
return cloneElement( children, { ...props, order } );
|
||||||
|
}
|
||||||
|
throw Error( 'Invalid children type' );
|
||||||
|
}
|
||||||
|
export { createOrderedChildren };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort fills by order for slot children.
|
||||||
|
*
|
||||||
|
* @param {Array} fills - slot's `Fill`s.
|
||||||
|
* @return {Node} Node.
|
||||||
|
*/
|
||||||
|
export const sortFillsByOrder: Slot.Props[ 'children' ] = ( fills ) => {
|
||||||
|
// Copy fills array here because its type is readonly array that doesn't have .sort method in Typescript definition.
|
||||||
|
const sortedFills = [ ...fills ].sort( ( a, b ) => {
|
||||||
|
return a[ 0 ].props.order - b[ 0 ].props.order;
|
||||||
|
} );
|
||||||
|
|
||||||
|
return <Fragment>{ sortedFills }</Fragment>;
|
||||||
|
};
|
|
@ -0,0 +1,42 @@
|
||||||
|
# WooProductFieldItem Slot & Fill
|
||||||
|
|
||||||
|
A Slotfill component that will allow you to add a new field to a specific section in the product editor.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<WooProductFieldItem id={ key } section="details" order={ 2 } pluginId="test-plugin" >
|
||||||
|
{ () => {
|
||||||
|
return (
|
||||||
|
<TextControl
|
||||||
|
label="Name"
|
||||||
|
name={ `product-mvp-name` }
|
||||||
|
placeholder="e.g. 12 oz Coffee Mug"
|
||||||
|
value="Test Name"
|
||||||
|
onChange={ () => console.debug( 'Changed!' ) }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</WooProductFieldItem>
|
||||||
|
|
||||||
|
<WooProductFieldItem.Slot section="details" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### WooProductFieldItem (fill)
|
||||||
|
|
||||||
|
This is the fill component. You must provide the `id` prop to identify your product field fill with a unique string. This component will accept a series of props:
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
| -------------| -------- | ------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `id` | String | A unique string to identify your fill. Used for configuiration management. |
|
||||||
|
| `section ` | String | The string used to identify the particular section where you want to render your field. |
|
||||||
|
| `pluginId` | String | A unique plugin ID to identify the plugin/extension that this fill is associated with. |
|
||||||
|
| `order` | Number | (optional) This number will dictate the order that the fields rendered by a Slot will be appear. |
|
||||||
|
|
||||||
|
### WooProductFieldItem.Slot (slot)
|
||||||
|
|
||||||
|
This is the slot component, and will not be used as frequently. It must also receive the required `location` prop that will be identical to the fill `location`.
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ----------- | ------ | ---------------------------------------------------------------------------------------------------- |
|
||||||
|
| `section` | String | Unique to the section that the Slot appears, and must be the same as the one provided to any fills. |
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './woo-product-field-item';
|
|
@ -0,0 +1,58 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { Slot, Fill } from '@wordpress/components';
|
||||||
|
import { createElement, Children } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { createOrderedChildren, sortFillsByOrder } from '../utils';
|
||||||
|
|
||||||
|
type WooProductFieldItemProps = {
|
||||||
|
id: string;
|
||||||
|
section: string;
|
||||||
|
pluginId: string;
|
||||||
|
order?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WooProductFieldSlotProps = {
|
||||||
|
section: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
|
||||||
|
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
|
||||||
|
} = ( { children, order = 1, section } ) => (
|
||||||
|
<Fill name={ `woocommerce_product_field_${ section }` }>
|
||||||
|
{ ( fillProps: Fill.Props ) => {
|
||||||
|
return createOrderedChildren< Fill.Props >(
|
||||||
|
children,
|
||||||
|
order,
|
||||||
|
fillProps
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</Fill>
|
||||||
|
);
|
||||||
|
|
||||||
|
WooProductFieldItem.Slot = ( { fillProps, section } ) => (
|
||||||
|
<Slot
|
||||||
|
name={ `woocommerce_product_field_${ section }` }
|
||||||
|
fillProps={ fillProps }
|
||||||
|
>
|
||||||
|
{ ( fills ) => {
|
||||||
|
if ( ! sortFillsByOrder ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Children.map(
|
||||||
|
sortFillsByOrder( fills )?.props.children,
|
||||||
|
( child ) => (
|
||||||
|
<div className="woocommerce-product-form__field">
|
||||||
|
{ child }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</Slot>
|
||||||
|
);
|
|
@ -1,31 +0,0 @@
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { WooProductFieldItem } from './woo-product-field-item';
|
|
||||||
|
|
||||||
type ProductFieldLayoutProps = {
|
|
||||||
fieldName: string;
|
|
||||||
categoryName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProductFieldLayout: React.FC< ProductFieldLayoutProps > = ( {
|
|
||||||
fieldName,
|
|
||||||
categoryName,
|
|
||||||
children,
|
|
||||||
} ) => {
|
|
||||||
return (
|
|
||||||
<div className="product-field-layout">
|
|
||||||
<WooProductFieldItem.Slot
|
|
||||||
fieldName={ fieldName }
|
|
||||||
categoryName={ categoryName }
|
|
||||||
location="before"
|
|
||||||
/>
|
|
||||||
{ children }
|
|
||||||
<WooProductFieldItem.Slot
|
|
||||||
fieldName={ fieldName }
|
|
||||||
categoryName={ categoryName }
|
|
||||||
location="after"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -22,7 +22,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-product-form__field {
|
.woocommerce-product-form__field:not(:first-child) {
|
||||||
margin-top: $gap-large;
|
margin-top: $gap-large;
|
||||||
|
|
||||||
> .components-base-control {
|
> .components-base-control {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { FormSection } from '@woocommerce/components';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './product-section-layout.scss';
|
import './product-section-layout.scss';
|
||||||
import { ProductFieldLayout } from './product-field-layout';
|
|
||||||
|
|
||||||
type ProductSectionLayoutProps = {
|
type ProductSectionLayoutProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -31,12 +30,7 @@ export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( {
|
||||||
{ Children.map( children, ( child ) => {
|
{ Children.map( children, ( child ) => {
|
||||||
if ( isValidElement( child ) && child.props.onChange ) {
|
if ( isValidElement( child ) && child.props.onChange ) {
|
||||||
return (
|
return (
|
||||||
<ProductFieldLayout
|
<div className="product-field-layout">{ child }</div>
|
||||||
fieldName={ child.props.name }
|
|
||||||
categoryName={ title }
|
|
||||||
>
|
|
||||||
{ child }
|
|
||||||
</ProductFieldLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return child;
|
return child;
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import { SlotFillProvider } from '@wordpress/components';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { ProductFieldLayout } from '../product-field-layout';
|
|
||||||
import { WooProductFieldItem } from '../woo-product-field-item';
|
|
||||||
|
|
||||||
describe( 'ProductFieldLayout', () => {
|
|
||||||
beforeEach( () => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'should allow adding extra fields before the field using slot fill', () => {
|
|
||||||
const { queryByText } = render(
|
|
||||||
<SlotFillProvider>
|
|
||||||
<ProductFieldLayout
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Product Details"
|
|
||||||
>
|
|
||||||
<div>Name field</div>
|
|
||||||
</ProductFieldLayout>
|
|
||||||
<div>
|
|
||||||
<WooProductFieldItem
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Product Details"
|
|
||||||
location="before"
|
|
||||||
>
|
|
||||||
<div>New field</div>
|
|
||||||
</WooProductFieldItem>
|
|
||||||
</div>
|
|
||||||
</SlotFillProvider>
|
|
||||||
);
|
|
||||||
expect( queryByText( 'New field' ) ).toBeInTheDocument();
|
|
||||||
expect( queryByText( 'New field' )?.nextSibling?.textContent ).toEqual(
|
|
||||||
'Name field'
|
|
||||||
);
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'should allow adding extra fields after the field using slot fill', () => {
|
|
||||||
const { queryByText } = render(
|
|
||||||
<SlotFillProvider>
|
|
||||||
<ProductFieldLayout
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Product Details"
|
|
||||||
>
|
|
||||||
<div>Name field</div>
|
|
||||||
</ProductFieldLayout>
|
|
||||||
<div>
|
|
||||||
<WooProductFieldItem
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Product Details"
|
|
||||||
location="after"
|
|
||||||
>
|
|
||||||
<div>New field</div>
|
|
||||||
</WooProductFieldItem>
|
|
||||||
</div>
|
|
||||||
</SlotFillProvider>
|
|
||||||
);
|
|
||||||
expect( queryByText( 'New field' ) ).toBeInTheDocument();
|
|
||||||
expect( queryByText( 'Name field' )?.nextSibling?.textContent ).toEqual(
|
|
||||||
'New field'
|
|
||||||
);
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'should not render new slot fills when field name does not match', () => {
|
|
||||||
const { queryByText } = render(
|
|
||||||
<SlotFillProvider>
|
|
||||||
<ProductFieldLayout
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Product Details"
|
|
||||||
>
|
|
||||||
<div>Name field</div>
|
|
||||||
</ProductFieldLayout>
|
|
||||||
<div>
|
|
||||||
<WooProductFieldItem
|
|
||||||
fieldName="Description"
|
|
||||||
categoryName="Product Details"
|
|
||||||
location="after"
|
|
||||||
>
|
|
||||||
<div>New field</div>
|
|
||||||
</WooProductFieldItem>
|
|
||||||
</div>
|
|
||||||
</SlotFillProvider>
|
|
||||||
);
|
|
||||||
expect( queryByText( 'New field' ) ).not.toBeInTheDocument();
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'should not render new slot fills when category name does not match', () => {
|
|
||||||
const { queryByText } = render(
|
|
||||||
<SlotFillProvider>
|
|
||||||
<ProductFieldLayout
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Product Details"
|
|
||||||
>
|
|
||||||
<div>Name field</div>
|
|
||||||
</ProductFieldLayout>
|
|
||||||
<div>
|
|
||||||
<WooProductFieldItem
|
|
||||||
fieldName="Name"
|
|
||||||
categoryName="Images"
|
|
||||||
location="after"
|
|
||||||
>
|
|
||||||
<div>New field</div>
|
|
||||||
</WooProductFieldItem>
|
|
||||||
</div>
|
|
||||||
</SlotFillProvider>
|
|
||||||
);
|
|
||||||
expect( queryByText( 'New field' ) ).not.toBeInTheDocument();
|
|
||||||
} );
|
|
||||||
} );
|
|
|
@ -8,28 +8,10 @@ import { render } from '@testing-library/react';
|
||||||
*/
|
*/
|
||||||
import { ProductSectionLayout } from '../product-section-layout';
|
import { ProductSectionLayout } from '../product-section-layout';
|
||||||
|
|
||||||
jest.mock( '../product-field-layout', () => {
|
|
||||||
const productFieldLayoutMock: React.FC< {
|
|
||||||
fieldName: string;
|
|
||||||
categoryName: string;
|
|
||||||
} > = ( { children, fieldName, categoryName } ) => {
|
|
||||||
return (
|
|
||||||
<div className="product-field-layout-mock">
|
|
||||||
<span>fieldName: { fieldName }</span>
|
|
||||||
<span>categoryName: { categoryName }</span>
|
|
||||||
{ children }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
ProductFieldLayout: productFieldLayoutMock,
|
|
||||||
};
|
|
||||||
} );
|
|
||||||
|
|
||||||
const SampleInputField: React.FC< { name: string; onChange: () => void } > = ( {
|
const SampleInputField: React.FC< { name: string; onChange: () => void } > = ( {
|
||||||
name,
|
name,
|
||||||
} ) => {
|
} ) => {
|
||||||
return <div>smaple-input-field-{ name }</div>;
|
return <div>sample-input-field-{ name }</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe( 'ProductSectionLayout', () => {
|
describe( 'ProductSectionLayout', () => {
|
||||||
|
@ -59,12 +41,9 @@ describe( 'ProductSectionLayout', () => {
|
||||||
</ProductSectionLayout>
|
</ProductSectionLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect( queryByText( 'fieldName: name' ) ).toBeInTheDocument();
|
expect( queryByText( 'sample-input-field-name' ) ).toBeInTheDocument();
|
||||||
expect( queryAllByText( 'categoryName: Title' ).length ).toEqual( 2 );
|
|
||||||
|
|
||||||
expect( queryByText( 'smaple-input-field-name' ) ).toBeInTheDocument();
|
|
||||||
expect(
|
expect(
|
||||||
queryByText( 'smaple-input-field-description' )
|
queryByText( 'sample-input-field-description' )
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { Slot, Fill } from '@wordpress/components';
|
|
||||||
import { snakeCase } from 'lodash';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { createOrderedChildren, sortFillsByOrder } from '~/utils';
|
|
||||||
|
|
||||||
// TODO: move this to a published JS package once ready.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Fill for extensions to add items to the Product edit page.
|
|
||||||
*
|
|
||||||
* @slotFill WooProductFieldItem
|
|
||||||
* @scope woocommerce-admin
|
|
||||||
* @example
|
|
||||||
* const MyProductDetailsFieldItem = () => (
|
|
||||||
* <WooProductFieldItem fieldName="name" categoryName="Product details" location="after">My header item</WooProductFieldItem>
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* registerPlugin( 'my-extension', {
|
|
||||||
* render: MyProductDetailsFieldItem,
|
|
||||||
* scope: 'woocommerce-admin',
|
|
||||||
* } );
|
|
||||||
* @param {Object} param0
|
|
||||||
* @param {Array} param0.children - Node children.
|
|
||||||
* @param {string} param0.fieldName - Field name.
|
|
||||||
* @param {string} param0.categoryName - Category name.
|
|
||||||
* @param {number} param0.order - Order of Fill component.
|
|
||||||
* @param {string} param0.location - Location before or after.
|
|
||||||
*/
|
|
||||||
export const WooProductFieldItem: React.FC< {
|
|
||||||
fieldName: string;
|
|
||||||
categoryName: string;
|
|
||||||
order?: number;
|
|
||||||
location: 'before' | 'after';
|
|
||||||
} > & {
|
|
||||||
Slot: React.FC<
|
|
||||||
Slot.Props & {
|
|
||||||
fieldName: string;
|
|
||||||
categoryName: string;
|
|
||||||
location: 'before' | 'after';
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
} = ( { children, fieldName, categoryName, location, order = 1 } ) => {
|
|
||||||
const categoryKey = snakeCase( categoryName );
|
|
||||||
const fieldKey = snakeCase( fieldName );
|
|
||||||
return (
|
|
||||||
<Fill
|
|
||||||
name={ `woocommerce_product_${ categoryKey }_${ fieldKey }_${ location }` }
|
|
||||||
>
|
|
||||||
{ ( fillProps: Fill.Props ) => {
|
|
||||||
return createOrderedChildren( children, order, fillProps );
|
|
||||||
} }
|
|
||||||
</Fill>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
WooProductFieldItem.Slot = ( {
|
|
||||||
fillProps,
|
|
||||||
fieldName,
|
|
||||||
categoryName,
|
|
||||||
location,
|
|
||||||
} ) => {
|
|
||||||
const categoryKey = snakeCase( categoryName );
|
|
||||||
const fieldKey = snakeCase( fieldName );
|
|
||||||
return (
|
|
||||||
<Slot
|
|
||||||
name={ `woocommerce_product_${ categoryKey }_${ fieldKey }_${ location }` }
|
|
||||||
fillProps={ fillProps }
|
|
||||||
>
|
|
||||||
{ sortFillsByOrder }
|
|
||||||
</Slot>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
useFormContext,
|
useFormContext,
|
||||||
__experimentalRichTextEditor as RichTextEditor,
|
__experimentalRichTextEditor as RichTextEditor,
|
||||||
__experimentalTooltip as Tooltip,
|
__experimentalTooltip as Tooltip,
|
||||||
|
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||||
} from '@woocommerce/components';
|
} from '@woocommerce/components';
|
||||||
import interpolateComponents from '@automattic/interpolate-components';
|
import interpolateComponents from '@automattic/interpolate-components';
|
||||||
import {
|
import {
|
||||||
|
@ -241,6 +242,7 @@ export const ProductDetailsSection: React.FC = () => {
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
) }
|
) }
|
||||||
/>
|
/>
|
||||||
|
<WooProductFieldItem.Slot section="details" />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</ProductSectionLayout>
|
</ProductSectionLayout>
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Adding WooProductFieldItem slot to product details section.
|
Loading…
Reference in New Issue