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:
louwie17 2024-09-04 10:52:51 +02:00 committed by GitHub
parent f8677c45d1
commit 907b591a42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 358 additions and 61 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add DataForms panel to the experimental product data views list.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,19 @@
/** /**
* External dependencies * External dependencies
*/ */
import { Action, DataViews, View } from '@wordpress/dataviews'; import { DataViews, View } from '@wordpress/dataviews';
import { import {
createElement, createElement,
useState, useState,
useMemo, useMemo,
useCallback, useCallback,
useEffect, useEffect,
Fragment,
} from '@wordpress/element'; } from '@wordpress/element';
import { Product, ProductQuery } from '@woocommerce/data'; import { Product, ProductQuery } from '@woocommerce/data';
import { drawerRight } from '@wordpress/icons'; import { drawerRight } from '@wordpress/icons';
import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as routerPrivateApis } from '@wordpress/router';
import { store as coreStore } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import classNames from 'classnames'; import classNames from 'classnames';
@ -39,63 +41,13 @@ import {
useDefaultViews, useDefaultViews,
defaultLayouts, defaultLayouts,
} from '../sidebar-dataviews/default-views'; } 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 { 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 = { export type ProductListProps = {
subTitle?: string; subTitle?: string;
className?: string; className?: string;
@ -105,7 +57,6 @@ export type ProductListProps = {
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
const EMPTY_ARRAY: Product[] = []; const EMPTY_ARRAY: Product[] = [];
const EMPTY_ACTIONS_ARRAY: Action< Product >[] = [];
const getDefaultView = ( const getDefaultView = (
defaultViews: Array< { slug: string; view: View } >, defaultViews: Array< { slug: string; view: View } >,
@ -265,6 +216,33 @@ export default function ProductList( {
[ totalCount, view.perPage ] [ 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 ); const classes = classNames( 'edit-site-page', className );
return ( return (
@ -290,7 +268,18 @@ export default function ProductList( {
{ __( 'Products', 'woocommerce' ) } { __( 'Products', 'woocommerce' ) }
</Heading> </Heading>
<FlexItem className="edit-site-page-header__actions"> <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> </FlexItem>
</HStack> </HStack>
{ subTitle && ( { subTitle && (
@ -307,12 +296,11 @@ export default function ProductList( {
<DataViews <DataViews
key={ activeView + isCustom } key={ activeView + isCustom }
paginationInfo={ paginationInfo } paginationInfo={ paginationInfo }
// @ts-expect-error types seem rather strict for this still. fields={ productFields }
fields={ fields }
actions={ EMPTY_ACTIONS_ARRAY }
data={ records || EMPTY_ARRAY } data={ records || EMPTY_ARRAY }
isLoading={ isLoading } isLoading={ isLoading }
view={ view } view={ view }
actions={ actions }
onChangeView={ setView } onChangeView={ setView }
onChangeSelection={ onChangeSelection } onChangeSelection={ onChangeSelection }
getItemId={ getItemId } getItemId={ getItemId }

View File

@ -9,6 +9,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router';
*/ */
import { unlock } from '../lock-unlock'; import { unlock } from '../lock-unlock';
import ProductList from './product-list'; import ProductList from './product-list';
import ProductEdit from './product-edit';
import DataViewsSidebarContent from './sidebar-dataviews'; import DataViewsSidebarContent from './sidebar-dataviews';
import SidebarNavigationScreen from './sidebar-navigation-screen'; import SidebarNavigationScreen from './sidebar-navigation-screen';
@ -32,7 +33,13 @@ export type Route = {
export default function useLayoutAreas() { export default function useLayoutAreas() {
const { params = {} } = useLocation(); const { params = {} } = useLocation();
const { postType = 'product', layout = 'table', canvas } = params; const {
postType = 'product',
layout = 'table',
canvas,
quickEdit: showQuickEdit,
postId,
} = params;
// Products list. // Products list.
if ( [ 'product' ].includes( postType ) ) { if ( [ 'product' ].includes( postType ) ) {
const isListLayout = layout === 'list' || ! layout; const isListLayout = layout === 'list' || ! layout;
@ -49,9 +56,13 @@ export default function useLayoutAreas() {
content: <ProductList />, content: <ProductList />,
preview: false, preview: false,
mobile: <ProductList postType={ postType } />, mobile: <ProductList postType={ postType } />,
edit: showQuickEdit && (
<ProductEdit postType={ postType } postId={ postId } />
),
}, },
widths: { widths: {
content: isListLayout ? 380 : undefined, content: isListLayout ? 380 : undefined,
edit: showQuickEdit && ! isListLayout ? 380 : undefined,
}, },
}; };
} }

View File

@ -41,3 +41,4 @@ body.js.is-fullscreen-mode {
} }
@import "products-app/sidebar-dataviews/style.scss"; @import "products-app/sidebar-dataviews/style.scss";
@import "products-app/product-edit/style.scss";