Add product data forms to data views (#51071)
* Add DataForms for products * Fix list view * Revert pnpm lock changes * Add changelog * Reference core store versus using string * Revert pnpm-lock
This commit is contained in:
parent
f8677c45d1
commit
907b591a42
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add DataForms panel to the experimental product data views list.
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import { edit } from '@wordpress/icons';
|
||||
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { unlock } from '../../lock-unlock';
|
||||
|
||||
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||
|
||||
export const useEditProductAction = ( { postType }: { postType: string } ) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
return useMemo(
|
||||
() => ( {
|
||||
id: 'edit-product',
|
||||
label: __( 'Edit', 'woocommerce' ),
|
||||
isPrimary: true,
|
||||
icon: edit,
|
||||
supportsBulk: true,
|
||||
isEligible( product: Product ) {
|
||||
if ( product.status === 'trash' ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
callback( items: Product[] ) {
|
||||
const product = items[ 0 ];
|
||||
history.push( {
|
||||
...location.params,
|
||||
postId: product.id,
|
||||
postType,
|
||||
quickEdit: true,
|
||||
} );
|
||||
},
|
||||
} ),
|
||||
[ history, location.params ]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { DataForm, isItemValid } from '@wordpress/dataviews';
|
||||
import type { Form } from '@wordpress/dataviews';
|
||||
import { createElement, useState, useMemo } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalHeading as Heading,
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalText as Text,
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalHStack as HStack,
|
||||
// @ts-expect-error missing types.
|
||||
__experimentalVStack as VStack,
|
||||
FlexItem,
|
||||
Button,
|
||||
} from '@wordpress/components';
|
||||
// @ts-expect-error missing types.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { privateApis as editorPrivateApis } from '@wordpress/editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { unlock } from '../../lock-unlock';
|
||||
import { productFields } from '../product-list/fields';
|
||||
|
||||
const { NavigableRegion } = unlock( editorPrivateApis );
|
||||
|
||||
const form: Form = {
|
||||
type: 'panel',
|
||||
fields: [ 'name', 'status' ],
|
||||
};
|
||||
|
||||
type ProductEditProps = {
|
||||
subTitle?: string;
|
||||
className?: string;
|
||||
hideTitleFromUI?: boolean;
|
||||
actions?: React.JSX.Element;
|
||||
postType: string;
|
||||
postId: string;
|
||||
};
|
||||
|
||||
export default function ProductEdit( {
|
||||
subTitle,
|
||||
actions,
|
||||
className,
|
||||
hideTitleFromUI = true,
|
||||
postType,
|
||||
postId = '',
|
||||
}: ProductEditProps ) {
|
||||
const classes = classNames( 'edit-product-page', className, {
|
||||
'is-empty': ! postId,
|
||||
} );
|
||||
const ids = useMemo( () => postId.split( ',' ), [ postId ] );
|
||||
const { initialEdits } = useSelect(
|
||||
( select ) => {
|
||||
return {
|
||||
initialEdits:
|
||||
ids.length === 1
|
||||
? select( 'wc/admin/products' ).getProduct( ids[ 0 ] )
|
||||
: null,
|
||||
};
|
||||
},
|
||||
[ postType, ids ]
|
||||
);
|
||||
const [ edits, setEdits ] = useState( {} );
|
||||
const itemWithEdits = useMemo( () => {
|
||||
return {
|
||||
...initialEdits,
|
||||
...edits,
|
||||
};
|
||||
}, [ initialEdits, edits ] );
|
||||
const isUpdateDisabled = ! isItemValid(
|
||||
itemWithEdits,
|
||||
productFields,
|
||||
form
|
||||
);
|
||||
|
||||
const onSubmit = async ( event: Event ) => {
|
||||
event.preventDefault();
|
||||
|
||||
if ( ! isItemValid( itemWithEdits, productFields, form ) ) {
|
||||
return;
|
||||
}
|
||||
// Empty save.
|
||||
|
||||
setEdits( {} );
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigableRegion
|
||||
className={ classes }
|
||||
ariaLabel={ __( 'Product Edit', 'woocommerce' ) }
|
||||
>
|
||||
<div className="edit-product-content">
|
||||
{ ! hideTitleFromUI && (
|
||||
<VStack
|
||||
className="edit-site-page-header"
|
||||
as="header"
|
||||
spacing={ 0 }
|
||||
>
|
||||
<HStack className="edit-site-page-header__page-title">
|
||||
<Heading
|
||||
as="h2"
|
||||
level={ 3 }
|
||||
weight={ 500 }
|
||||
className="edit-site-page-header__title"
|
||||
truncate
|
||||
>
|
||||
{ __( 'Product Edit', 'woocommerce' ) }
|
||||
</Heading>
|
||||
<FlexItem className="edit-site-page-header__actions">
|
||||
{ actions }
|
||||
</FlexItem>
|
||||
</HStack>
|
||||
{ subTitle && (
|
||||
<Text
|
||||
variant="muted"
|
||||
as="p"
|
||||
className="edit-site-page-header__sub-title"
|
||||
>
|
||||
{ subTitle }
|
||||
</Text>
|
||||
) }
|
||||
</VStack>
|
||||
) }
|
||||
{ ! postId && (
|
||||
<p>{ __( 'Select a product to edit', 'woocommerce' ) }</p>
|
||||
) }
|
||||
{ postId && (
|
||||
<VStack spacing={ 4 } as="form" onSubmit={ onSubmit }>
|
||||
<DataForm
|
||||
data={ itemWithEdits }
|
||||
fields={ productFields }
|
||||
form={ form }
|
||||
onChange={ setEdits }
|
||||
/>
|
||||
<FlexItem>
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
// @ts-expect-error missing type.
|
||||
accessibleWhenDisabled
|
||||
disabled={ isUpdateDisabled }
|
||||
__next40pxDefaultSize
|
||||
>
|
||||
{ __( 'Update', 'woocommerce' ) }
|
||||
</Button>
|
||||
</FlexItem>
|
||||
</VStack>
|
||||
) }
|
||||
</div>
|
||||
</NavigableRegion>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
.edit-product-page {
|
||||
padding: $grid-unit-30;
|
||||
background: #fff;
|
||||
color: #2f2f2f;
|
||||
container: edit-site-page/inline-size;
|
||||
height: 100%;
|
||||
transition: width .2s ease-out;
|
||||
|
||||
.edit-product-content {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.is-empty .edit-product-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Field } from '@wordpress/dataviews';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { OPERATOR_IS } from '../constants';
|
||||
|
||||
const STATUSES = [
|
||||
{ value: 'draft', label: __( 'Draft', 'woocommerce' ) },
|
||||
{ value: 'future', label: __( 'Scheduled', 'woocommerce' ) },
|
||||
{ value: 'private', label: __( 'Private', 'woocommerce' ) },
|
||||
{ value: 'publish', label: __( 'Published', 'woocommerce' ) },
|
||||
{ value: 'trash', label: __( 'Trash', 'woocommerce' ) },
|
||||
];
|
||||
|
||||
/**
|
||||
* TODO: auto convert some of the product editor blocks ( from the blocks directory ) to this format.
|
||||
* The edit function should work relatively well with the edit from the blocks, the only difference is that the blocks rely on getEntityProp to get the value
|
||||
*/
|
||||
export const productFields: Field< Product >[] = [
|
||||
{
|
||||
id: 'name',
|
||||
label: __( 'Name', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
type: 'text',
|
||||
render: function nameRender( { item }: { item: Product } ) {
|
||||
return <>{ item.name }</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sku',
|
||||
label: __( 'SKU', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return <>{ item.sku }</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'date',
|
||||
label: __( 'Date', 'woocommerce' ),
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return <time>{ item.date_created }</time>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __( 'Status', 'woocommerce' ),
|
||||
id: 'status',
|
||||
getValue: ( { item }: { item: Product } ) =>
|
||||
STATUSES.find( ( { value } ) => value === item.status )?.label ??
|
||||
item.status,
|
||||
elements: STATUSES,
|
||||
filterBy: {
|
||||
operators: [ OPERATOR_IS ],
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
|
@ -1,17 +1,19 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Action, DataViews, View } from '@wordpress/dataviews';
|
||||
import { DataViews, View } from '@wordpress/dataviews';
|
||||
import {
|
||||
createElement,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
Fragment,
|
||||
} from '@wordpress/element';
|
||||
import { Product, ProductQuery } from '@woocommerce/data';
|
||||
import { drawerRight } from '@wordpress/icons';
|
||||
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||
import { store as coreStore } from '@wordpress/core-data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import classNames from 'classnames';
|
||||
|
@ -39,63 +41,13 @@ import {
|
|||
useDefaultViews,
|
||||
defaultLayouts,
|
||||
} from '../sidebar-dataviews/default-views';
|
||||
import { LAYOUT_LIST, OPERATOR_IS } from '../constants';
|
||||
import { LAYOUT_LIST } from '../constants';
|
||||
import { productFields } from './fields';
|
||||
import { useEditProductAction } from '../dataviews-actions';
|
||||
|
||||
const { NavigableRegion } = unlock( editorPrivateApis );
|
||||
const { NavigableRegion, usePostActions } = unlock( editorPrivateApis );
|
||||
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||
|
||||
const STATUSES = [
|
||||
{ value: 'draft', label: __( 'Draft', 'woocommerce' ) },
|
||||
{ value: 'future', label: __( 'Scheduled', 'woocommerce' ) },
|
||||
{ value: 'private', label: __( 'Private', 'woocommerce' ) },
|
||||
{ value: 'publish', label: __( 'Published', 'woocommerce' ) },
|
||||
{ value: 'trash', label: __( 'Trash', 'woocommerce' ) },
|
||||
];
|
||||
|
||||
/**
|
||||
* TODO: auto convert some of the product editor blocks ( from the blocks directory ) to this format.
|
||||
* The edit function should work relatively well with the edit from the blocks, the only difference is that the blocks rely on getEntityProp to get the value
|
||||
*/
|
||||
const fields = [
|
||||
{
|
||||
id: 'name',
|
||||
label: __( 'Name', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
type: 'text',
|
||||
render: function nameRender( { item }: { item: Product } ) {
|
||||
return item.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sku',
|
||||
label: __( 'SKU', 'woocommerce' ),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return item.sku;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'date',
|
||||
label: __( 'Date', 'woocommerce' ),
|
||||
render: ( { item }: { item: Product } ) => {
|
||||
return <time>{ item.date_created }</time>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: __( 'Status', 'woocommerce' ),
|
||||
id: 'status',
|
||||
getValue: ( { item }: { item: Product } ) =>
|
||||
STATUSES.find( ( { value } ) => value === item.status )?.label ??
|
||||
item.status,
|
||||
elements: STATUSES,
|
||||
filterBy: {
|
||||
operators: [ OPERATOR_IS ],
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
|
||||
export type ProductListProps = {
|
||||
subTitle?: string;
|
||||
className?: string;
|
||||
|
@ -105,7 +57,6 @@ export type ProductListProps = {
|
|||
|
||||
const PAGE_SIZE = 25;
|
||||
const EMPTY_ARRAY: Product[] = [];
|
||||
const EMPTY_ACTIONS_ARRAY: Action< Product >[] = [];
|
||||
|
||||
const getDefaultView = (
|
||||
defaultViews: Array< { slug: string; view: View } >,
|
||||
|
@ -265,6 +216,33 @@ export default function ProductList( {
|
|||
[ totalCount, view.perPage ]
|
||||
);
|
||||
|
||||
const { labels, canCreateRecord } = useSelect(
|
||||
( select ) => {
|
||||
const { getPostType, canUser } = select( coreStore );
|
||||
const postTypeData:
|
||||
| { labels: Record< string, string > }
|
||||
| undefined = getPostType( postType );
|
||||
return {
|
||||
labels: postTypeData?.labels,
|
||||
canCreateRecord: canUser( 'create', {
|
||||
kind: 'postType',
|
||||
name: postType,
|
||||
} ),
|
||||
};
|
||||
},
|
||||
[ postType ]
|
||||
);
|
||||
|
||||
const postTypeActions = usePostActions( {
|
||||
postType,
|
||||
context: 'list',
|
||||
} );
|
||||
const editAction = useEditProductAction( { postType } );
|
||||
const actions = useMemo(
|
||||
() => [ editAction, ...postTypeActions ],
|
||||
[ postTypeActions, editAction ]
|
||||
);
|
||||
|
||||
const classes = classNames( 'edit-site-page', className );
|
||||
|
||||
return (
|
||||
|
@ -290,7 +268,18 @@ export default function ProductList( {
|
|||
{ __( 'Products', 'woocommerce' ) }
|
||||
</Heading>
|
||||
<FlexItem className="edit-site-page-header__actions">
|
||||
{ /* { actions } */ }
|
||||
{ labels?.add_new_item && canCreateRecord && (
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={ true }
|
||||
// @ts-expect-error missing type.
|
||||
__next40pxDefaultSize
|
||||
>
|
||||
{ labels.add_new_item }
|
||||
</Button>
|
||||
</>
|
||||
) }
|
||||
</FlexItem>
|
||||
</HStack>
|
||||
{ subTitle && (
|
||||
|
@ -307,12 +296,11 @@ export default function ProductList( {
|
|||
<DataViews
|
||||
key={ activeView + isCustom }
|
||||
paginationInfo={ paginationInfo }
|
||||
// @ts-expect-error types seem rather strict for this still.
|
||||
fields={ fields }
|
||||
actions={ EMPTY_ACTIONS_ARRAY }
|
||||
fields={ productFields }
|
||||
data={ records || EMPTY_ARRAY }
|
||||
isLoading={ isLoading }
|
||||
view={ view }
|
||||
actions={ actions }
|
||||
onChangeView={ setView }
|
||||
onChangeSelection={ onChangeSelection }
|
||||
getItemId={ getItemId }
|
||||
|
|
|
@ -9,6 +9,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
|
|||
*/
|
||||
import { unlock } from '../lock-unlock';
|
||||
import ProductList from './product-list';
|
||||
import ProductEdit from './product-edit';
|
||||
import DataViewsSidebarContent from './sidebar-dataviews';
|
||||
import SidebarNavigationScreen from './sidebar-navigation-screen';
|
||||
|
||||
|
@ -32,7 +33,13 @@ export type Route = {
|
|||
|
||||
export default function useLayoutAreas() {
|
||||
const { params = {} } = useLocation();
|
||||
const { postType = 'product', layout = 'table', canvas } = params;
|
||||
const {
|
||||
postType = 'product',
|
||||
layout = 'table',
|
||||
canvas,
|
||||
quickEdit: showQuickEdit,
|
||||
postId,
|
||||
} = params;
|
||||
// Products list.
|
||||
if ( [ 'product' ].includes( postType ) ) {
|
||||
const isListLayout = layout === 'list' || ! layout;
|
||||
|
@ -49,9 +56,13 @@ export default function useLayoutAreas() {
|
|||
content: <ProductList />,
|
||||
preview: false,
|
||||
mobile: <ProductList postType={ postType } />,
|
||||
edit: showQuickEdit && (
|
||||
<ProductEdit postType={ postType } postId={ postId } />
|
||||
),
|
||||
},
|
||||
widths: {
|
||||
content: isListLayout ? 380 : undefined,
|
||||
edit: showQuickEdit && ! isListLayout ? 380 : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,3 +41,4 @@ body.js.is-fullscreen-mode {
|
|||
}
|
||||
|
||||
@import "products-app/sidebar-dataviews/style.scss";
|
||||
@import "products-app/product-edit/style.scss";
|
||||
|
|
Loading…
Reference in New Issue