Add/36075 render fields sections from php config (#36414)
* Add initial component to auto load fills from API config * Add changelog * Update logic to make use of new store and re-usable components * Add changelog * Add loading state for product form data to add/edit product pages
This commit is contained in:
parent
025c6aab17
commit
fc1745b03b
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
|
||||||
|
Tweak the product form types and exports.
|
|
@ -76,7 +76,11 @@ export {
|
||||||
// Export types
|
// Export types
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './countries/types';
|
export * from './countries/types';
|
||||||
export { ProductForm } from './product-form/types';
|
export {
|
||||||
|
ProductForm,
|
||||||
|
ProductFormField,
|
||||||
|
ProductFormSection,
|
||||||
|
} from './product-form/types';
|
||||||
export * from './onboarding/types';
|
export * from './onboarding/types';
|
||||||
export * from './plugins/types';
|
export * from './plugins/types';
|
||||||
export * from './products/types';
|
export * from './products/types';
|
||||||
|
@ -172,6 +176,7 @@ import { ProductCategorySelectors } from './product-categories/types';
|
||||||
import { ProductAttributeTermsSelectors } from './product-attribute-terms/types';
|
import { ProductAttributeTermsSelectors } from './product-attribute-terms/types';
|
||||||
import { ProductVariationSelectors } from './product-variations/types';
|
import { ProductVariationSelectors } from './product-variations/types';
|
||||||
import { TaxClassSelectors } from './tax-classes/types';
|
import { TaxClassSelectors } from './tax-classes/types';
|
||||||
|
import { ProductFormSelectors } from './product-form/selectors';
|
||||||
|
|
||||||
// As we add types to all the package selectors we can fill out these unknown types with real ones. See one
|
// As we add types to all the package selectors we can fill out these unknown types with real ones. See one
|
||||||
// of the already typed selectors for an example of how you can do this.
|
// of the already typed selectors for an example of how you can do this.
|
||||||
|
@ -219,6 +224,8 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME
|
||||||
? ShippingZonesSelectors
|
? ShippingZonesSelectors
|
||||||
: T extends typeof EXPERIMENTAL_TAX_CLASSES_STORE_NAME
|
: T extends typeof EXPERIMENTAL_TAX_CLASSES_STORE_NAME
|
||||||
? TaxClassSelectors
|
? TaxClassSelectors
|
||||||
|
: T extends typeof EXPERIMENTAL_PRODUCT_FORM_STORE_NAME
|
||||||
|
? ProductFormSelectors
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export interface WCDataSelector {
|
export interface WCDataSelector {
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import TYPES from './action-types';
|
import TYPES from './action-types';
|
||||||
import { Field, ProductForm } from './types';
|
import { ProductFormField, ProductForm } from './types';
|
||||||
|
|
||||||
export function getFieldsSuccess( fields: Field[] ) {
|
export function getFieldsSuccess( fields: ProductFormField[] ) {
|
||||||
return {
|
return {
|
||||||
type: TYPES.GET_FIELDS_SUCCESS as const,
|
type: TYPES.GET_FIELDS_SUCCESS as const,
|
||||||
fields,
|
fields,
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
getProductFormError,
|
getProductFormError,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { WC_ADMIN_NAMESPACE } from '../constants';
|
import { WC_ADMIN_NAMESPACE } from '../constants';
|
||||||
import { Field, ProductForm } from './types';
|
import { ProductFormField, ProductForm } from './types';
|
||||||
import { STORE_NAME } from './constants';
|
import { STORE_NAME } from './constants';
|
||||||
|
|
||||||
const resolveSelect =
|
const resolveSelect =
|
||||||
|
@ -23,7 +23,7 @@ const resolveSelect =
|
||||||
export function* getFields() {
|
export function* getFields() {
|
||||||
try {
|
try {
|
||||||
const url = WC_ADMIN_NAMESPACE + '/product-form/fields';
|
const url = WC_ADMIN_NAMESPACE + '/product-form/fields';
|
||||||
const results: Field[] = yield apiFetch( {
|
const results: ProductFormField[] = yield apiFetch( {
|
||||||
path: url,
|
path: url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { WPDataSelector, WPDataSelectors } from '../types';
|
||||||
import { ProductFormState } from './types';
|
import { ProductFormState } from './types';
|
||||||
|
|
||||||
export const getFields = ( state: ProductFormState ) => {
|
export const getFields = ( state: ProductFormState ) => {
|
||||||
|
@ -15,3 +16,9 @@ export const getProductForm = ( state: ProductFormState ) => {
|
||||||
const { errors, ...form } = state;
|
const { errors, ...form } = state;
|
||||||
return form;
|
return form;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProductFormSelectors = {
|
||||||
|
getFields: WPDataSelector< typeof getFields >;
|
||||||
|
getField: WPDataSelector< typeof getField >;
|
||||||
|
getProductForm: WPDataSelector< typeof getProductForm >;
|
||||||
|
} & WPDataSelectors;
|
||||||
|
|
|
@ -9,22 +9,23 @@ type FieldProperties = {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Field = BaseComponent & {
|
export type ProductFormField = BaseComponent & {
|
||||||
type: string;
|
type: string;
|
||||||
section: string;
|
section: string;
|
||||||
properties: FieldProperties;
|
properties: FieldProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Section = BaseComponent & {
|
export type ProductFormSection = BaseComponent & {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
location: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Subsection = BaseComponent;
|
export type Subsection = BaseComponent;
|
||||||
|
|
||||||
export type ProductForm = {
|
export type ProductForm = {
|
||||||
fields: Field[];
|
fields: ProductFormField[];
|
||||||
sections: Section[];
|
sections: ProductFormSection[];
|
||||||
subsections: Subsection[];
|
subsections: Subsection[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,13 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
import { useEffect } from '@wordpress/element';
|
import { useEffect } from '@wordpress/element';
|
||||||
|
import { Spinner } from '@wordpress/components';
|
||||||
|
import {
|
||||||
|
EXPERIMENTAL_PRODUCT_FORM_STORE_NAME,
|
||||||
|
WCDataSelector,
|
||||||
|
} from '@woocommerce/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -10,16 +16,32 @@ import { useEffect } from '@wordpress/element';
|
||||||
import { ProductForm } from './product-form';
|
import { ProductForm } from './product-form';
|
||||||
import { ProductTourContainer } from './tour';
|
import { ProductTourContainer } from './tour';
|
||||||
import './product-page.scss';
|
import './product-page.scss';
|
||||||
|
import './fills';
|
||||||
|
|
||||||
const AddProductPage: React.FC = () => {
|
const AddProductPage: React.FC = () => {
|
||||||
|
const { isLoading } = useSelect( ( select: WCDataSelector ) => {
|
||||||
|
const { hasFinishedResolution: hasProductFormFinishedResolution } =
|
||||||
|
select( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME );
|
||||||
|
return {
|
||||||
|
isLoading: ! hasProductFormFinishedResolution( 'getProductForm' ),
|
||||||
|
};
|
||||||
|
} );
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
recordEvent( 'view_new_product_management_experience' );
|
recordEvent( 'view_new_product_management_experience' );
|
||||||
}, [] );
|
}, [] );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-add-product">
|
<div className="woocommerce-add-product">
|
||||||
<ProductForm />
|
{ isLoading ? (
|
||||||
<ProductTourContainer />
|
<div className="woocommerce-edit-product__spinner">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ProductForm />
|
||||||
|
<ProductTourContainer />
|
||||||
|
</>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
|
EXPERIMENTAL_PRODUCT_FORM_STORE_NAME,
|
||||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||||
PartialProduct,
|
PartialProduct,
|
||||||
Product,
|
Product,
|
||||||
|
@ -21,6 +22,7 @@ import { ProductForm } from './product-form';
|
||||||
import { ProductFormLayout } from './layout/product-form-layout';
|
import { ProductFormLayout } from './layout/product-form-layout';
|
||||||
import { ProductVariationForm } from './product-variation-form';
|
import { ProductVariationForm } from './product-variation-form';
|
||||||
import './product-page.scss';
|
import './product-page.scss';
|
||||||
|
import './fills';
|
||||||
|
|
||||||
const EditProductPage: React.FC = () => {
|
const EditProductPage: React.FC = () => {
|
||||||
const { productId, variationId } = useParams();
|
const { productId, variationId } = useParams();
|
||||||
|
@ -35,6 +37,8 @@ const EditProductPage: React.FC = () => {
|
||||||
isPending,
|
isPending,
|
||||||
getPermalinkParts,
|
getPermalinkParts,
|
||||||
} = select( PRODUCTS_STORE_NAME );
|
} = select( PRODUCTS_STORE_NAME );
|
||||||
|
const { hasFinishedResolution: hasProductFormFinishedResolution } =
|
||||||
|
select( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME );
|
||||||
const {
|
const {
|
||||||
getProductVariation,
|
getProductVariation,
|
||||||
hasFinishedResolution: hasProductVariationFinishedResolution,
|
hasFinishedResolution: hasProductVariationFinishedResolution,
|
||||||
|
@ -71,7 +75,8 @@ const EditProductPage: React.FC = () => {
|
||||||
'getProductVariation',
|
'getProductVariation',
|
||||||
[ parseInt( variationId, 10 ) ]
|
[ parseInt( variationId, 10 ) ]
|
||||||
)
|
)
|
||||||
),
|
) ||
|
||||||
|
! hasProductFormFinishedResolution( 'getProductForm' ),
|
||||||
isPendingAction:
|
isPendingAction:
|
||||||
isPending( 'createProduct' ) ||
|
isPending( 'createProduct' ) ||
|
||||||
isPending(
|
isPending(
|
||||||
|
|
|
@ -28,6 +28,7 @@ const DetailsSection = () => (
|
||||||
id={ DETAILS_SECTION_ID }
|
id={ DETAILS_SECTION_ID }
|
||||||
location="tab/general"
|
location="tab/general"
|
||||||
pluginId="core"
|
pluginId="core"
|
||||||
|
order={ 1 }
|
||||||
>
|
>
|
||||||
<ProductFieldSection
|
<ProductFieldSection
|
||||||
id="general/details"
|
id="general/details"
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './product-form-fills';
|
||||||
|
|
||||||
export * from './details-section/details-section-fills';
|
export * from './details-section/details-section-fills';
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||||
|
renderField,
|
||||||
|
useFormContext,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import { Product, ProductFormField } from '@woocommerce/data';
|
||||||
|
|
||||||
|
export const Fields: React.FC< { fields: ProductFormField[] } > = ( {
|
||||||
|
fields,
|
||||||
|
} ) => {
|
||||||
|
const { getInputProps } = useFormContext< Product >();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ fields.map( ( field ) => (
|
||||||
|
<WooProductFieldItem
|
||||||
|
key={ field.properties.name }
|
||||||
|
id={ field.id }
|
||||||
|
section={ field.section }
|
||||||
|
pluginId={ field.plugin_id }
|
||||||
|
order={ field.order }
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{ renderField( field.type, {
|
||||||
|
...getInputProps( field.properties.name ),
|
||||||
|
...field.properties,
|
||||||
|
} ) }
|
||||||
|
</>
|
||||||
|
</WooProductFieldItem>
|
||||||
|
) ) }{ ' ' }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { registerPlugin } from '@wordpress/plugins';
|
||||||
|
import { useSelect, resolveSelect } from '@wordpress/data';
|
||||||
|
import {
|
||||||
|
EXPERIMENTAL_PRODUCT_FORM_STORE_NAME,
|
||||||
|
WCDataSelector,
|
||||||
|
} from '@woocommerce/data';
|
||||||
|
import { registerCoreProductFields } from '@woocommerce/components';
|
||||||
|
|
||||||
|
registerCoreProductFields();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { Fields } from './product-form-field-fills';
|
||||||
|
import { Sections } from './product-form-section-fills';
|
||||||
|
|
||||||
|
const Form = () => {
|
||||||
|
const { formData } = useSelect( ( select: WCDataSelector ) => {
|
||||||
|
return {
|
||||||
|
formData: select(
|
||||||
|
EXPERIMENTAL_PRODUCT_FORM_STORE_NAME
|
||||||
|
).getProductForm(),
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ formData && (
|
||||||
|
<>
|
||||||
|
<Sections sections={ formData.sections } />
|
||||||
|
<Fields fields={ formData.fields } />
|
||||||
|
</>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preloading product form data, as product pages are waiting on this to be resolved.
|
||||||
|
* The above Form component won't get rendered until the getProductForm is resolved.
|
||||||
|
*/
|
||||||
|
resolveSelect( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME ).getProductForm();
|
||||||
|
registerPlugin( 'wc-admin-product-editor-form-fills', {
|
||||||
|
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||||
|
scope: 'woocommerce-product-editor',
|
||||||
|
render: () => {
|
||||||
|
return <Form />;
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Card, CardBody } from '@wordpress/components';
|
||||||
|
import {
|
||||||
|
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||||
|
__experimentalProductFieldSection as ProductFieldSection,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import { ProductFormSection } from '@woocommerce/data';
|
||||||
|
|
||||||
|
export const Sections: React.FC< { sections: ProductFormSection[] } > = ( {
|
||||||
|
sections,
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ sections.map( ( section ) => (
|
||||||
|
<WooProductSectionItem
|
||||||
|
key={ section.id }
|
||||||
|
id={ section.id }
|
||||||
|
location={ section.location }
|
||||||
|
pluginId={ section.plugin_id }
|
||||||
|
order={ section.order }
|
||||||
|
>
|
||||||
|
<ProductFieldSection
|
||||||
|
id={ section.id }
|
||||||
|
title={ section.title }
|
||||||
|
description={ section.description }
|
||||||
|
/>
|
||||||
|
</WooProductSectionItem>
|
||||||
|
) ) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -7,8 +7,8 @@ import {
|
||||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||||
} from '@woocommerce/components';
|
} from '@woocommerce/components';
|
||||||
import { PartialProduct, Product } from '@woocommerce/data';
|
import { PartialProduct, Product } from '@woocommerce/data';
|
||||||
import { Ref } from 'react';
|
|
||||||
import { PluginArea } from '@wordpress/plugins';
|
import { PluginArea } from '@wordpress/plugins';
|
||||||
|
import { Ref } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -26,8 +26,6 @@ import { OptionsSection } from './sections/options-section';
|
||||||
import { ProductFormFooter } from './layout/product-form-footer';
|
import { ProductFormFooter } from './layout/product-form-footer';
|
||||||
import { ProductFormTab } from './product-form-tab';
|
import { ProductFormTab } from './product-form-tab';
|
||||||
|
|
||||||
import './fills';
|
|
||||||
|
|
||||||
export const ProductForm: React.FC< {
|
export const ProductForm: React.FC< {
|
||||||
product?: PartialProduct;
|
product?: PartialProduct;
|
||||||
formRef?: Ref< FormRef< Partial< Product > > >;
|
formRef?: Ref< FormRef< Partial< Product > > >;
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
|
||||||
|
Minor adjustments to the ProductForm API
|
Loading…
Reference in New Issue