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:
louwie17 2023-01-19 04:52:02 -04:00 committed by GitHub
parent 025c6aab17
commit fc1745b03b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 192 additions and 15 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Tweak the product form types and exports.

View File

@ -76,7 +76,11 @@ export {
// Export types
export * from './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 './plugins/types';
export * from './products/types';
@ -172,6 +176,7 @@ import { ProductCategorySelectors } from './product-categories/types';
import { ProductAttributeTermsSelectors } from './product-attribute-terms/types';
import { ProductVariationSelectors } from './product-variations/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
// 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
: T extends typeof EXPERIMENTAL_TAX_CLASSES_STORE_NAME
? TaxClassSelectors
: T extends typeof EXPERIMENTAL_PRODUCT_FORM_STORE_NAME
? ProductFormSelectors
: never;
export interface WCDataSelector {

View File

@ -2,9 +2,9 @@
* Internal dependencies
*/
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 {
type: TYPES.GET_FIELDS_SUCCESS as const,
fields,

View File

@ -14,7 +14,7 @@ import {
getProductFormError,
} from './actions';
import { WC_ADMIN_NAMESPACE } from '../constants';
import { Field, ProductForm } from './types';
import { ProductFormField, ProductForm } from './types';
import { STORE_NAME } from './constants';
const resolveSelect =
@ -23,7 +23,7 @@ const resolveSelect =
export function* getFields() {
try {
const url = WC_ADMIN_NAMESPACE + '/product-form/fields';
const results: Field[] = yield apiFetch( {
const results: ProductFormField[] = yield apiFetch( {
path: url,
method: 'GET',
} );

View File

@ -1,6 +1,7 @@
/**
* Internal dependencies
*/
import { WPDataSelector, WPDataSelectors } from '../types';
import { ProductFormState } from './types';
export const getFields = ( state: ProductFormState ) => {
@ -15,3 +16,9 @@ export const getProductForm = ( state: ProductFormState ) => {
const { errors, ...form } = state;
return form;
};
export type ProductFormSelectors = {
getFields: WPDataSelector< typeof getFields >;
getField: WPDataSelector< typeof getField >;
getProductForm: WPDataSelector< typeof getProductForm >;
} & WPDataSelectors;

View File

@ -9,22 +9,23 @@ type FieldProperties = {
label: string;
};
export type Field = BaseComponent & {
export type ProductFormField = BaseComponent & {
type: string;
section: string;
properties: FieldProperties;
};
export type Section = BaseComponent & {
export type ProductFormSection = BaseComponent & {
title: string;
description: string;
location: string;
};
export type Subsection = BaseComponent;
export type ProductForm = {
fields: Field[];
sections: Section[];
fields: ProductFormField[];
sections: ProductFormSection[];
subsections: Subsection[];
};

View File

@ -2,7 +2,13 @@
* External dependencies
*/
import { recordEvent } from '@woocommerce/tracks';
import { useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { Spinner } from '@wordpress/components';
import {
EXPERIMENTAL_PRODUCT_FORM_STORE_NAME,
WCDataSelector,
} from '@woocommerce/data';
/**
* Internal dependencies
@ -10,16 +16,32 @@ import { useEffect } from '@wordpress/element';
import { ProductForm } from './product-form';
import { ProductTourContainer } from './tour';
import './product-page.scss';
import './fills';
const AddProductPage: React.FC = () => {
const { isLoading } = useSelect( ( select: WCDataSelector ) => {
const { hasFinishedResolution: hasProductFormFinishedResolution } =
select( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME );
return {
isLoading: ! hasProductFormFinishedResolution( 'getProductForm' ),
};
} );
useEffect( () => {
recordEvent( 'view_new_product_management_experience' );
}, [] );
return (
<div className="woocommerce-add-product">
<ProductForm />
<ProductTourContainer />
{ isLoading ? (
<div className="woocommerce-edit-product__spinner">
<Spinner />
</div>
) : (
<>
<ProductForm />
<ProductTourContainer />
</>
) }
</div>
);
};

View File

@ -3,6 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import {
EXPERIMENTAL_PRODUCT_FORM_STORE_NAME,
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
PartialProduct,
Product,
@ -21,6 +22,7 @@ import { ProductForm } from './product-form';
import { ProductFormLayout } from './layout/product-form-layout';
import { ProductVariationForm } from './product-variation-form';
import './product-page.scss';
import './fills';
const EditProductPage: React.FC = () => {
const { productId, variationId } = useParams();
@ -35,6 +37,8 @@ const EditProductPage: React.FC = () => {
isPending,
getPermalinkParts,
} = select( PRODUCTS_STORE_NAME );
const { hasFinishedResolution: hasProductFormFinishedResolution } =
select( EXPERIMENTAL_PRODUCT_FORM_STORE_NAME );
const {
getProductVariation,
hasFinishedResolution: hasProductVariationFinishedResolution,
@ -71,7 +75,8 @@ const EditProductPage: React.FC = () => {
'getProductVariation',
[ parseInt( variationId, 10 ) ]
)
),
) ||
! hasProductFormFinishedResolution( 'getProductForm' ),
isPendingAction:
isPending( 'createProduct' ) ||
isPending(

View File

@ -28,6 +28,7 @@ const DetailsSection = () => (
id={ DETAILS_SECTION_ID }
location="tab/general"
pluginId="core"
order={ 1 }
>
<ProductFieldSection
id="general/details"

View File

@ -1 +1,6 @@
/**
* Internal dependencies
*/
import './product-form-fills';
export * from './details-section/details-section-fills';

View File

@ -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>
) ) }{ ' ' }
</>
);
};

View File

@ -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 />;
},
} );

View File

@ -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>
) ) }
</>
);
};

View File

@ -7,8 +7,8 @@ import {
__experimentalWooProductSectionItem as WooProductSectionItem,
} from '@woocommerce/components';
import { PartialProduct, Product } from '@woocommerce/data';
import { Ref } from 'react';
import { PluginArea } from '@wordpress/plugins';
import { Ref } from 'react';
/**
* Internal dependencies
@ -26,8 +26,6 @@ import { OptionsSection } from './sections/options-section';
import { ProductFormFooter } from './layout/product-form-footer';
import { ProductFormTab } from './product-form-tab';
import './fills';
export const ProductForm: React.FC< {
product?: PartialProduct;
formRef?: Ref< FormRef< Partial< Product > > >;

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Minor adjustments to the ProductForm API