Create variation items block (#39657)

* Create and register product-variation-items-field block

* Create variations-table component

* Use variations-table component in variation-items block

* Remove last border bottom from the variation options list

* Add changelog file

* Add changelog file
This commit is contained in:
Maikel David Pérez Gómez 2023-08-09 15:06:59 -04:00 committed by GitHub
parent f36cb3a50c
commit 3ffe7b8376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 565 additions and 6 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product variation items block

View File

@ -23,4 +23,5 @@ export { init as initToggle } from './toggle';
export { init as attributesInit } from './attributes';
export { init as initVariations } from './variations';
export { init as initRequirePassword } from './password';
export { init as initVariationItems } from './variation-items';
export { init as initVariationOptions } from './variation-options';

View File

@ -15,4 +15,5 @@
@import 'tab/editor.scss';
@import 'variations/editor.scss';
@import 'password/editor.scss';
@import 'variation-items/editor.scss';
@import 'variation-options/editor.scss';

View File

@ -0,0 +1,26 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-variation-items-field",
"title": "Product variations items",
"category": "woocommerce",
"description": "The product variations items.",
"keywords": [ "products", "variations" ],
"textdomain": "default",
"attributes": {
"description": {
"type": "string",
"__experimentalRole": "content"
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false,
"__experimentalToolbar": false
},
"editorStyle": "file:./editor.css"
}

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { VariationsTable } from '../../components/variations-table';
export function Edit() {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<VariationsTable />
</div>
);
}

View File

@ -0,0 +1,2 @@
.wp-block-woocommerce-product-variations-items-field {
}

View File

@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { initBlock } from '../../utils/init-blocks';
import blockConfiguration from './block.json';
import { Edit } from './edit';
import { VariationOptionsBlockAttributes } from './types';
const { name, ...metadata } =
blockConfiguration as BlockConfiguration< VariationOptionsBlockAttributes >;
export { metadata, name };
export const settings: Partial<
BlockConfiguration< VariationOptionsBlockAttributes >
> = {
example: {},
edit: Edit,
};
export function init() {
return initBlock( { name, metadata, settings } );
}

View File

@ -0,0 +1,8 @@
/**
* External dependencies
*/
import { BlockAttributes } from '@wordpress/blocks';
export interface VariationOptionsBlockAttributes extends BlockAttributes {
description: string;
}

View File

@ -1,17 +1,20 @@
.wp-block-woocommerce-product-variations-options-field {
.woocommerce-sortable {
.woocommerce-sortable {
padding: 0;
&__item:not(:last-child) .woocommerce-list-item {
border-bottom: 1px solid $gray-200;
}
}
.woocommerce-list-item {
background: none;
border: none;
border-bottom: 1px solid $gray-200;
padding-left: 0;
grid-template-columns: 26% auto 90px;
grid-template-columns: 26% auto 90px;
}
.woocommerce-sortable__handle {
display: none;
}
.woocommerce-sortable__handle {
display: none;
}
}

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export default function HiddenIcon( {
width = 24,
height = 24,
...props
}: React.SVGProps< SVGSVGElement > ) {
return (
<svg
{ ...props }
width={ width }
height={ height }
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.7226 6.2125C13.1641 6.0766 12.5883 6 11.9999 6C8.10055 6 4.75407 9.36447 3.31899 11.0546C2.8507 11.6061 2.8507 12.3939 3.31899 12.9454C4.17896 13.9582 5.72533 15.5723 7.66574 16.7033L8.41572 15.4043C8.13761 15.242 7.86389 15.0655 7.59553 14.8776C6.25019 13.9359 5.15775 12.7905 4.48406 12C5.15775 11.2095 6.25019 10.0641 7.59553 9.12235C8.96667 8.16257 10.4775 7.5 11.9999 7.5C12.3118 7.5 12.6231 7.5278 12.9329 7.58027L13.7226 6.2125ZM12.3504 8.58923C12.2352 8.57753 12.1182 8.57153 11.9999 8.57153C10.1063 8.57153 8.57132 10.1066 8.57132 12.0001C8.57132 12.7505 8.81237 13.4445 9.22126 14.0091L10.1233 12.4467C10.0893 12.3034 10.0713 12.1538 10.0713 12.0001C10.0713 11.1266 10.652 10.3888 11.4484 10.1515L12.3504 8.58923ZM12.8092 10.2491L13.5611 8.94679C14.6697 9.51479 15.4285 10.6688 15.4285 12.0001C15.4285 13.8937 13.8934 15.4287 11.9999 15.4287C11.3128 15.4287 10.6729 15.2266 10.1364 14.8785L10.8883 13.5763C11.2025 13.7983 11.5859 13.9287 11.9999 13.9287C13.065 13.9287 13.9285 13.0652 13.9285 12.0001C13.9285 11.224 13.4701 10.555 12.8092 10.2491ZM9.51376 15.957C10.3246 16.2986 11.1605 16.5 11.9999 16.5C13.5223 16.5 15.0331 15.8374 16.4043 14.8776C17.7496 13.9359 18.842 12.7905 19.5157 12C18.842 11.2095 17.7496 10.0641 16.4043 9.12235C15.6875 8.62066 14.9327 8.20018 14.1579 7.91308L14.917 6.59839C17.5164 7.64275 19.6204 9.80575 20.6808 11.0546C21.1491 11.6061 21.1491 12.3939 20.6808 12.9454C19.2457 14.6355 15.8992 18 11.9999 18C10.8611 18 9.76945 17.713 8.7588 17.2646L9.51376 15.957Z"
fill="currentColor"
/>
<rect
x="16.0625"
y="4.61377"
width="1.22727"
height="16"
transform="rotate(30 16.0625 4.61377)"
fill="currentColor"
/>
</svg>
);
}

View File

@ -0,0 +1 @@
export * from './variations-table';

View File

@ -0,0 +1,103 @@
.woocommerce-product-variations {
ol {
@media ( min-width: #{ ($break-medium) } ) {
min-height: 420px;
}
}
display: flex;
flex-direction: column;
> div {
display: flex;
flex-direction: column;
flex-grow: 1;
}
&__status-dot {
margin-right: $gap-smaller;
&.green {
color: $alert-green;
}
&.yellow {
color: $alert-yellow;
}
&.red {
color: $alert-red;
}
}
&__price--fade,
&__quantity--fade {
opacity: 0.5;
}
&__actions {
display: flex;
align-items: center;
justify-content: flex-end;
.components-button {
position: relative;
color: var(--wp-admin-theme-color);
&:disabled,
&[aria-disabled="true"] {
opacity: 1;
}
.components-spinner {
margin: 4px;
}
}
.components-button svg {
fill: none;
}
.components-button--visible {
color: $gray-700;
}
.components-button--hidden {
color: $alert-red;
}
}
.woocommerce-list-item {
display: grid;
grid-template-columns: auto 25% 25% 88px;
padding: 0;
min-height: calc($grid-unit * 9);
border: none;
}
.woocommerce-sortable {
margin: 0;
flex: 1 0 auto;
&__item:not(:last-child) .woocommerce-list-item {
border-bottom: 1px solid $gray-200;
}
&__handle {
display: none;
}
}
&.is-loading {
min-height: 476px;
.components-spinner {
width: 34px;
height: 34px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
position: absolute;
margin: 0;
}
}
&__footer {
padding: $gap;
}
}

View File

@ -0,0 +1,266 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Spinner, Tooltip } from '@wordpress/components';
import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
ProductVariation,
} from '@woocommerce/data';
import {
Link,
ListItem,
Pagination,
Sortable,
Tag,
} from '@woocommerce/components';
import { getNewPath } from '@woocommerce/navigation';
import { useContext, useState, createElement } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import classnames from 'classnames';
import truncate from 'lodash/truncate';
import { CurrencyContext } from '@woocommerce/currency';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityId } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import HiddenIcon from './hidden-icon';
import VisibleIcon from './visible-icon';
import { getProductStockStatus, getProductStockStatusClass } from '../../utils';
import {
DEFAULT_PER_PAGE_OPTION,
PRODUCT_VARIATION_TITLE_LIMIT,
} from '../../constants';
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
const VISIBLE_TEXT = __( 'Visible to customers', 'woocommerce' );
const UPDATING_TEXT = __( 'Updating product variation', 'woocommerce' );
export function VariationsTable() {
const [ currentPage, setCurrentPage ] = useState( 1 );
const [ perPage, setPerPage ] = useState( DEFAULT_PER_PAGE_OPTION );
const [ isUpdating, setIsUpdating ] = useState< Record< string, boolean > >(
{}
);
const productId = useEntityId( 'postType', 'product' );
const context = useContext( CurrencyContext );
const { formatAmount } = context;
const { isLoading, variations, totalCount } = useSelect(
( select ) => {
const {
getProductVariations,
hasFinishedResolution,
getProductVariationsTotalCount,
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const requestParams = {
product_id: productId,
page: currentPage,
per_page: perPage,
order: 'asc',
orderby: 'menu_order',
};
return {
isLoading: ! hasFinishedResolution( 'getProductVariations', [
requestParams,
] ),
variations:
getProductVariations< ProductVariation[] >( requestParams ),
totalCount:
getProductVariationsTotalCount< number >( requestParams ),
};
},
[ currentPage, perPage, productId ]
);
const { updateProductVariation } = useDispatch(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
);
if ( ! variations || isLoading ) {
return (
<div className="woocommerce-product-variations is-loading">
<Spinner />
</div>
);
}
function handleCustomerVisibilityClick(
variationId: number,
status: 'private' | 'publish'
) {
if ( isUpdating[ variationId ] ) return;
setIsUpdating( ( prevState ) => ( {
...prevState,
[ variationId ]: true,
} ) );
updateProductVariation< Promise< ProductVariation > >(
{ product_id: productId, id: variationId },
{ status }
).finally( () =>
setIsUpdating( ( prevState ) => ( {
...prevState,
[ variationId ]: false,
} ) )
);
}
return (
<div className="woocommerce-product-variations">
<Sortable>
{ variations.map( ( variation ) => (
<ListItem key={ `${ variation.id }` }>
<div className="woocommerce-product-variations__attributes">
{ variation.attributes.map( ( attribute ) => {
const tag = (
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
/* @ts-ignore Additional props are not required. */
<Tag
id={ attribute.id }
className="woocommerce-product-variations__attribute"
key={ attribute.id }
label={ truncate( attribute.option, {
length: PRODUCT_VARIATION_TITLE_LIMIT,
} ) }
screenReaderLabel={ attribute.option }
/>
);
return attribute.option.length <=
PRODUCT_VARIATION_TITLE_LIMIT ? (
tag
) : (
<Tooltip
key={ attribute.id }
text={ attribute.option }
position="top center"
>
<span>{ tag }</span>
</Tooltip>
);
} ) }
</div>
<div
className={ classnames(
'woocommerce-product-variations__price',
{
'woocommerce-product-variations__price--fade':
variation.status === 'private',
}
) }
>
{ formatAmount( variation.price ) }
</div>
<div
className={ classnames(
'woocommerce-product-variations__quantity',
{
'woocommerce-product-variations__quantity--fade':
variation.status === 'private',
}
) }
>
<span
className={ classnames(
'woocommerce-product-variations__status-dot',
getProductStockStatusClass( variation )
) }
>
</span>
{ getProductStockStatus( variation ) }
</div>
<div className="woocommerce-product-variations__actions">
{ variation.status === 'private' && (
<Tooltip
position="top center"
text={ NOT_VISIBLE_TEXT }
>
<Button
className="components-button--hidden"
aria-label={
isUpdating[ variation.id ]
? UPDATING_TEXT
: NOT_VISIBLE_TEXT
}
aria-disabled={
isUpdating[ variation.id ]
}
onClick={ () =>
handleCustomerVisibilityClick(
variation.id,
'publish'
)
}
>
{ isUpdating[ variation.id ] ? (
<Spinner />
) : (
<HiddenIcon />
) }
</Button>
</Tooltip>
) }
{ variation.status === 'publish' && (
<Tooltip
position="top center"
text={ VISIBLE_TEXT }
>
<Button
className="components-button--visible"
aria-label={
isUpdating[ variation.id ]
? UPDATING_TEXT
: VISIBLE_TEXT
}
aria-disabled={
isUpdating[ variation.id ]
}
onClick={ () =>
handleCustomerVisibilityClick(
variation.id,
'private'
)
}
>
{ isUpdating[ variation.id ] ? (
<Spinner />
) : (
<VisibleIcon />
) }
</Button>
</Tooltip>
) }
<Link
href={ getNewPath(
{},
`/product/${ productId }/variation/${ variation.id }`,
{}
) }
type="wc-admin"
className="components-button"
>
{ __( 'Edit', 'woocommerce' ) }
</Link>
</div>
</ListItem>
) ) }
</Sortable>
<Pagination
className="woocommerce-product-variations__footer"
page={ currentPage }
perPage={ perPage }
total={ totalCount }
showPagePicker={ false }
onPageChange={ setCurrentPage }
onPerPageChange={ setPerPage }
/>
</div>
);
}

View File

@ -0,0 +1,36 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export default function VisibleIcon( {
width = 24,
height = 24,
...props
}: React.SVGProps< SVGSVGElement > ) {
return (
<svg
{ ...props }
width={ width }
height={ height }
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M20.1091 11.54C20.3396 11.8116 20.3396 12.1884 20.1091 12.46C19.4144 13.2781 18.266 14.4899 16.8343 15.4921C15.397 16.4982 13.7359 17.25 11.9999 17.25C10.2638 17.25 8.60268 16.4982 7.1654 15.4921C5.73376 14.4899 4.58533 13.2781 3.89066 12.46C3.6601 12.1884 3.6601 11.8116 3.89066 11.54C4.58533 10.7219 5.73376 9.51006 7.1654 8.50792C8.60268 7.50184 10.2638 6.75 11.9999 6.75C13.7359 6.75 15.397 7.50184 16.8343 8.50792C18.266 9.51006 19.4144 10.7219 20.1091 11.54Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<circle
cx="11.9999"
cy="11.9999"
r="2.67857"
stroke="currentColor"
strokeWidth="1.5"
/>
</svg>
);
}

View File

@ -52,3 +52,13 @@ export const PRODUCT_DETAILS_SLUG = 'product-details';
export const PRODUCT_SCHEDULED_SALE_SLUG = 'product-scheduled-sale';
export const TRACKS_SOURCE = 'product-block-editor-v1';
/**
* Since the pagination component does not exposes the way of
* changing the per page options which are [25, 50, 75, 100]
* the default per page option will be the min in the list to
* keep compatibility.
*
* @see https://github.com/woocommerce/woocommerce/blob/trunk/packages/js/components/src/pagination/index.js#L12
*/
export const DEFAULT_PER_PAGE_OPTION = 25;

View File

@ -32,6 +32,7 @@
@import 'components/attribute-list-item/attribute-list-item.scss';
@import 'components/attribute-term-input-field/attribute-term-input-field.scss';
@import 'components/attribute-term-input-field/create-attribute-term-modal.scss';
@import 'components/variations-table/styles.scss';
/* Field Blocks */

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Register the product variation items block

View File

@ -42,6 +42,7 @@ class BlockRegistry {
'woocommerce/product-tab',
'woocommerce/product-inventory-quantity-field',
'woocommerce/product-toggle-field',
'woocommerce/product-variation-items-field',
'woocommerce/product-variations-fields',
'woocommerce/product-password-field',
];

View File

@ -761,6 +761,13 @@ class Init {
),
array( array( 'woocommerce/product-variations-options-field' ) ),
),
array(
'woocommerce/product-section',
array(
'title' => __( 'Variations', 'woocommerce' ),
),
array( array( 'woocommerce/product-variation-items-field' ) ),
),
),
),
),