Adding attributes block to product block editor. (#38051)

This commit is contained in:
Joel Thiessen 2023-05-02 21:13:48 -07:00 committed by GitHub
parent 94599a14cf
commit 3679f019bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 308 additions and 226 deletions

View File

@ -0,0 +1,4 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.545 14.4296L13.9351 20.0409C13.7898 20.1865 13.6172 20.3019 13.4272 20.3807C13.2373 20.4595 13.0336 20.5 12.828 20.5C12.6224 20.5 12.4187 20.4595 12.2288 20.3807C12.0388 20.3019 11.8662 20.1865 11.7209 20.0409L5 13.3261V5.5H12.8241L19.545 12.2226C19.8364 12.5159 20 12.9126 20 13.3261C20 13.7396 19.8364 14.1363 19.545 14.4296V14.4296Z" stroke="#1E1E1E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="9.5" r="1" fill="#1E1E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding attributes components, block and styles.

View File

@ -35,7 +35,7 @@
"@woocommerce/components": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/customer-effort-score": "workspace:*",
"@woocommerce/data": "workspace:^4.1.0",
"@woocommerce/data": "workspace:*",
"@woocommerce/experimental": "workspace:*",
"@woocommerce/navigation": "workspace:^8.1.0",
"@woocommerce/number": "workspace:*",
@ -89,6 +89,7 @@
"@types/wordpress__keycodes": "^2.3.1",
"@types/wordpress__media-utils": "^3.0.0",
"@types/wordpress__plugins": "^3.0.0",
"@types/wordpress__rich-text": "^3.4.6",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-js-tests": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",

View File

@ -0,0 +1,26 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-attributes-field",
"title": "Product attributes",
"category": "widgets",
"description": "The product attributes.",
"keywords": [ "products", "attributes" ],
"textdomain": "default",
"attributes": {
"name": {
"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,35 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { ProductAttribute } from '@woocommerce/data';
import { useBlockProps } from '@wordpress/block-editor';
// 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 { useEntityProp, useEntityId } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { Attributes as AttributesContainer } from '../../components/attributes/attributes';
export function Edit() {
const [ entityAttributes, setEntityAttributes ] = useEntityProp<
ProductAttribute[]
>( 'postType', 'product', 'attributes' );
const productId = useEntityId( 'postType', 'product' );
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<AttributesContainer
productId={ productId }
value={ entityAttributes }
onChange={ setEntityAttributes }
/>
</div>
);
}

View File

@ -0,0 +1,21 @@
.wp-block-woocommerce-product-images-field {
.woocommerce-image-gallery {
margin-top: $gap-largest;
}
.woocommerce-media-uploader {
text-align: left;
}
.woocommerce-media-uploader__label {
display: none;
}
.woocommerce-sortable {
margin-top: 0;
padding: 0;
}
&:not(.has-images) {
.woocommerce-sortable {
display: none;
}
}
}

View File

@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import { initBlock } from '../../utils';
import metadata from './block.json';
import { Edit } from './edit';
const { name } = metadata;
export { metadata, name };
export const settings = {
example: {},
edit: Edit,
};
export const init = () =>
initBlock( {
name,
metadata: metadata as never,
settings,
} );

View File

@ -17,3 +17,4 @@ export { init as initSummary } from './summary';
export { init as initTab } from './tab';
export { init as initInventoryQuantity } from './inventory-quantity';
export { init as initToggle } from './toggle';
export { init as attributesInit } from './attributes';

View File

@ -2,32 +2,32 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import {
useState,
createElement,
Fragment,
createInterpolateElement,
} from '@wordpress/element';
import { Button } from '@wordpress/components';
import { ProductAttribute } from '@woocommerce/data';
import {
Sortable,
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
Link,
} from '@woocommerce/components';
import interpolateComponents from '@automattic/interpolate-components';
import { getAdminLink } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import './attribute-field.scss';
import { EditAttributeModal } from './edit-attribute-modal';
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
import {
getAttributeId,
getAttributeKey,
reorderSortableProductAttributePositions,
} from './utils';
import { AttributeEmptyState } from '../attribute-empty-state';
import {
AttributeListItem,
NewAttributeListItem,
} from '../attribute-list-item';
import { AttributeListItem } from '../attribute-list-item';
import { NewAttributeModal } from './new-attribute-modal';
type AttributeControlProps = {
@ -67,9 +67,9 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
uiStrings = {
newAttributeModalTitle: undefined,
emptyStateSubtitle: undefined,
newAttributeListItemLabel: undefined,
newAttributeListItemLabel: __( 'Add attributes', 'woocommerce' ),
globalAttributeHelperMessage: __(
`You can change the attribute's name in {{link}}Attributes{{/link}}.`,
`You can change the attribute's name in <link>Attributes</link>.`,
'woocommerce'
),
},
@ -160,30 +160,6 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
closeEditModal( updatedAttribute );
};
if ( ! value.length ) {
return (
<>
<AttributeEmptyState
addNewLabel={ uiStrings.newAttributeModalTitle }
onNewClick={ () => openNewModal() }
subtitle={ uiStrings.emptyStateSubtitle }
/>
{ isNewModalVisible && (
<NewAttributeModal
onCancel={ () => {
closeNewModal();
onNewModalCancel();
} }
onAdd={ handleAdd }
selectedAttributeIds={ [] }
title={ uiStrings.newAttributeModalTitle }
/>
) }
<SelectControlMenuSlot />
</>
);
}
const sortedAttributes = value.sort( ( a, b ) => a.position - b.position );
const attributeKeyValues = value.reduce(
@ -203,37 +179,44 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
return (
<div className="woocommerce-attribute-field">
<Sortable
onOrderChange={ ( items ) => {
const itemPositions = items.reduce(
( positions, { props }, index ) => {
positions[ getAttributeKey( props.attribute ) ] =
index;
return positions;
},
{} as Record< number | string, number >
);
onChange(
reorderSortableProductAttributePositions(
itemPositions,
attributeKeyValues
)
);
} }
<Button
variant="secondary"
className="woocommerce-add-attribute-list-item__add-button"
onClick={ openNewModal }
>
{ sortedAttributes.map( ( attr ) => (
<AttributeListItem
attribute={ attr }
key={ getAttributeId( attr ) }
onEditClick={ () => openEditModal( attr ) }
onRemoveClick={ () => handleRemove( attr ) }
/>
) ) }
</Sortable>
<NewAttributeListItem
label={ uiStrings.newAttributeListItemLabel }
onClick={ () => openNewModal() }
/>
{ uiStrings.newAttributeListItemLabel }
</Button>
{ Boolean( value.length ) && (
<Sortable
onOrderChange={ ( items ) => {
const itemPositions = items.reduce(
( positions, { props }, index ) => {
positions[
getAttributeKey( props.attribute )
] = index;
return positions;
},
{} as Record< number | string, number >
);
onChange(
reorderSortableProductAttributePositions(
itemPositions,
attributeKeyValues
)
);
} }
>
{ sortedAttributes.map( ( attr ) => (
<AttributeListItem
attribute={ attr }
key={ getAttributeId( attr ) }
onEditClick={ () => openEditModal( attr ) }
onRemoveClick={ () => handleRemove( attr ) }
/>
) ) }
</Sortable>
) }
{ isNewModalVisible && (
<NewAttributeModal
title={ uiStrings.newAttributeModalTitle }
@ -253,9 +236,9 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
__( 'Edit %s', 'woocommerce' ),
currentAttribute.name
) }
globalAttributeHelperMessage={ interpolateComponents( {
mixedString: uiStrings.globalAttributeHelperMessage,
components: {
globalAttributeHelperMessage={ createInterpolateElement(
uiStrings.globalAttributeHelperMessage,
{
link: (
<Link
href={ getAdminLink(
@ -267,8 +250,8 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
<></>
</Link>
),
},
} ) }
}
) }
onCancel={ () => {
closeEditModal( currentAttribute );
onEditModalCancel( currentAttribute );

View File

@ -1,5 +1,6 @@
.woocommerce-attribute-field {
width: 100%;
font-size: 13px;
.woocommerce-sortable {
margin: 0;
@ -12,4 +13,23 @@
background: none;
border-top: 0;
}
.woocommerce-add-attribute-list-item__add-button {
margin-bottom: $gap;
}
}
.wp-block-woocommerce-product-attributes-field {
.woocommerce-sortable {
padding: 0;
}
.woocommerce-list-item {
background: none;
border: none;
border-bottom: 1px solid $gray-200;
padding-left: 0;
}
}

View File

@ -8,7 +8,7 @@ import {
CheckboxControl,
TextControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useState, createElement } from '@wordpress/element';
import { __experimentalTooltip as Tooltip } from '@woocommerce/components';
/**
@ -20,8 +20,6 @@ import {
} from '../attribute-term-input-field';
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
import './edit-attribute-modal.scss';
type EditAttributeModalProps = {
title?: string;
nameLabel?: string;

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState } from '@wordpress/element';
import { useState, createElement, Fragment } from '@wordpress/element';
import { trash } from '@wordpress/icons';
import {
Form,
@ -20,13 +20,12 @@ import {
/**
* Internal dependencies
*/
import './new-attribute-modal.scss';
import { AttributeInputField } from '../attribute-input-field';
import {
AttributeTermInputField,
CustomAttributeTermInputField,
} from '../attribute-term-input-field';
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
import { getProductAttributeObject } from './utils';
type NewAttributeModalProps = {

View File

@ -2,7 +2,12 @@
* External dependencies
*/
import { render, act, screen } from '@testing-library/react';
import { useState, useEffect } from '@wordpress/element';
import {
useState,
useEffect,
createElement,
Fragment,
} from '@wordpress/element';
import { ProductAttribute } from '@woocommerce/data';
/**
@ -108,12 +113,11 @@ describe( 'AttributeControl', () => {
} );
describe( 'empty state', () => {
it( 'should show subtitle and "Add first attribute" button', () => {
it( 'should show subtitle and "Add attributes" button', () => {
const { queryByText } = render(
<AttributeControl value={ [] } onChange={ () => {} } />
);
expect( queryByText( 'No attributes yet' ) ).toBeInTheDocument();
expect( queryByText( 'Add first attribute' ) ).toBeInTheDocument();
expect( queryByText( 'Add attributes' ) ).toBeInTheDocument();
} );
} );
@ -166,7 +170,7 @@ describe( 'AttributeControl', () => {
describe( 'deleting', () => {
it( 'should show a window confirm when trash icon is clicked', async () => {
jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false );
jest.spyOn( globalThis, 'confirm' ).mockReturnValueOnce( false );
act( () => {
render(
<AttributeControl
@ -178,11 +182,11 @@ describe( 'AttributeControl', () => {
(
await screen.findAllByLabelText( 'Remove attribute' )
)[ 0 ].click();
expect( global.confirm ).toHaveBeenCalled();
expect( globalThis.confirm ).toHaveBeenCalled();
} );
it( 'should trigger onChange with removed item when user clicks ok on alert', async () => {
jest.spyOn( global, 'confirm' ).mockReturnValueOnce( true );
jest.spyOn( globalThis, 'confirm' ).mockReturnValueOnce( true );
const onChange = jest.fn();
act( () => {
@ -198,12 +202,12 @@ describe( 'AttributeControl', () => {
await screen.findAllByLabelText( 'Remove attribute' )
)[ 0 ].click();
expect( global.confirm ).toHaveBeenCalled();
expect( globalThis.confirm ).toHaveBeenCalled();
expect( onChange ).toHaveBeenCalledWith( [ attributeList[ 1 ] ] );
} );
it( 'should not trigger onChange with removed item when user cancel', async () => {
jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false );
jest.spyOn( globalThis, 'confirm' ).mockReturnValueOnce( false );
const onChange = jest.fn();
act( () => {
render(
@ -216,14 +220,14 @@ describe( 'AttributeControl', () => {
(
await screen.findAllByLabelText( 'Remove attribute' )
)[ 0 ].click();
expect( global.confirm ).toHaveBeenCalled();
expect( globalThis.confirm ).toHaveBeenCalled();
expect( onChange ).not.toHaveBeenCalled();
} );
} );
describe( 'dragging', () => {
it.skip( 'should trigger onChange with new order when onOrderChange triggered', async () => {
jest.spyOn( global, 'confirm' ).mockReturnValueOnce( true );
jest.spyOn( globalThis, 'confirm' ).mockReturnValueOnce( true );
const onChange = jest.fn();
act( () => {

View File

@ -3,6 +3,7 @@
*/
import { render } from '@testing-library/react';
import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies

View File

@ -5,6 +5,7 @@ import { sprintf, __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { Spinner, Icon } from '@wordpress/components';
import { plus } from '@wordpress/icons';
import { createElement } from '@wordpress/element';
import {
EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME,
QueryProductAttribute,
@ -21,8 +22,7 @@ import {
/**
* Internal dependencies
*/
import './attribute-input-field.scss';
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >;
@ -51,6 +51,8 @@ export const AttributeInputField: React.FC< AttributeInputFieldProps > = ( {
disabled,
ignoredAttributeIds = [],
} ) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { attributes, isLoading } = useSelect( ( select: WCDataSelector ) => {
const { getProductAttributes, hasFinishedResolution } = select(
EXPERIMENTAL_PRODUCT_ATTRIBUTES_STORE_NAME

View File

@ -3,7 +3,7 @@
*/
import { render } from '@testing-library/react';
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { useState, createElement } from '@wordpress/element';
import { ProductAttribute, QueryProductAttribute } from '@woocommerce/data';
/**

View File

@ -4,6 +4,7 @@
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { ListItem } from '@woocommerce/components';
import { createElement } from '@wordpress/element';
type NewAttributeListItemProps = {
label?: string;

View File

@ -7,11 +7,7 @@ import { ProductAttribute } from '@woocommerce/data';
import { sprintf, __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { closeSmall } from '@wordpress/icons';
/**
* Internal dependencies
*/
import './attribute-list-item.scss';
import { createElement } from '@wordpress/element';
type AttributeListItemProps = {
attribute: ProductAttribute;

View File

@ -4,7 +4,14 @@
import { sprintf, __ } from '@wordpress/i18n';
import { CheckboxControl, Icon, Spinner } from '@wordpress/components';
import { resolveSelect } from '@wordpress/data';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import {
useCallback,
useEffect,
useRef,
useState,
createElement,
Fragment,
} from '@wordpress/element';
import { useDebounce } from '@wordpress/compose';
import { plus } from '@wordpress/icons';
import {
@ -21,7 +28,6 @@ import {
/**
* Internal dependencies
*/
import './attribute-term-input-field.scss';
import { CreateAttributeTermModal } from './create-attribute-term-modal';
type AttributeTermInputFieldProps = {

View File

@ -8,7 +8,7 @@ import {
TextareaControl,
TextControl,
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useState, createElement, Fragment } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { cleanForSlug } from '@wordpress/url';
import { Form, FormContextType, FormErrors } from '@woocommerce/components';
@ -19,11 +19,6 @@ import {
QueryProductAttribute,
} from '@woocommerce/data';
/**
* Internal dependencies
*/
import './create-attribute-term-modal.scss';
type CreateAttributeTermModalProps = {
initialAttributeTermName: string;
attributeId: number;

View File

@ -3,7 +3,7 @@
*/
import { sprintf, __ } from '@wordpress/i18n';
import { CheckboxControl, Icon } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useState, createElement, Fragment } from '@wordpress/element';
import { plus } from '@wordpress/icons';
import {
__experimentalSelectControl as SelectControl,
@ -11,11 +11,6 @@ import {
__experimentalSelectControlMenuItem as MenuItem,
} from '@woocommerce/components';
/**
* Internal dependencies
*/
import './attribute-term-input-field.scss';
type CustomAttributeTermInputFieldProps = {
value?: string[];
onChange: ( value: string[] ) => void;

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { act, render, waitFor, screen } from '@testing-library/react';
import { useState } from '@wordpress/element';
import { useState, createElement } from '@wordpress/element';
import { resolveSelect } from '@wordpress/data';
import { ProductAttributeTerm } from '@woocommerce/data';

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { ProductAttribute } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
@ -8,7 +9,7 @@ import { recordEvent } from '@woocommerce/tracks';
* Internal dependencies
*/
import { AttributeControl } from '../attribute-control';
import { useProductAttributes } from '~/products/hooks/use-product-attributes';
import { useProductAttributes } from '../../hooks/use-product-attributes';
type AttributesProps = {
value: ProductAttribute[];

View File

@ -9,6 +9,7 @@
button,
span,
label,
div,
input {
font-family: var( --wp--preset--font-family--system-font );
}

View File

@ -27,4 +27,7 @@ export {
type ShippingDimensionsImageProps,
type HighlightSides,
} from './shipping-dimensions-image';
export { AttributeControl as __experimentalAttributeControl } from './attribute-control';
export { Attributes as __experimentalAttributes } from './attributes';
export * from './add-new-shipping-class-modal';

View File

@ -12,7 +12,7 @@ import { useCallback, useEffect, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { sift } from '../../utils';
import { sift } from '../utils';
type useProductAttributesProps = {
allAttributes: ProductAttribute[];

View File

@ -24,6 +24,13 @@
@import 'components/edit-product-link-modal/style.scss';
@import 'components/details-categories-field/style.scss';
@import 'components/details-categories-field/create-category-modal.scss';
@import 'components/attribute-control/attribute-field.scss';
@import 'components/attribute-control/edit-attribute-modal.scss';
@import 'components/attribute-control/new-attribute-modal.scss';
@import 'components/attribute-input-field/attribute-input-field.scss';
@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';
/* Field Blocks */

View File

@ -23,6 +23,7 @@ export * from './create-ordered-children';
export * from './sort-fills-by-order';
export * from './init-blocks';
export * from './product-apifetch-middleware';
export * from './sift';
export {
AUTO_DRAFT_NAME,

View File

@ -0,0 +1,21 @@
type SiftResult< T > = [ T[], T[] ];
/**
* Similar to filter, but return two arrays separated by a partitioner function
*
* @param {Array} arr - Original array of values.
* @param {Function} partitioner - Function to return truthy/falsy values to separate items in array.
*
* @return {Array} - Array of two arrays, first including truthy values, and second including falsy.
*/
export const sift = < T >(
arr: Array< T >,
partitioner: ( item: T ) => boolean
): SiftResult< T > =>
arr.reduce< SiftResult< T > >(
( all, item ) => {
all[ !! partitioner( item ) ? 0 : 1 ].push( item );
return all;
},
[ [], [] ]
);

View File

@ -1,19 +0,0 @@
<svg width="151" height="72" viewBox="0 0 151 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="45.8291" y="25.2363" width="78.4945" height="45.2637" rx="3.7381" fill="#F6F7F7" stroke="#DDDDDD" stroke-width="3" stroke-dasharray="4 3"/>
<rect x="2.3125" y="1.5" width="78.4945" height="69" rx="3.7381" fill="white" stroke="#DDDDDD" stroke-width="3"/>
<line x1="11.3516" y1="9.94922" x2="33.6702" y2="9.94922" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<line x1="11.0156" y1="55.4668" x2="33.3343" y2="55.4668" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<line x1="11.0156" y1="61.0054" x2="26.2134" y2="61.0054" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<rect x="11.0156" y="41.0605" width="27.0659" height="8.07692" rx="1.5" stroke="#DDDDDD" stroke-width="3"/>
<line x1="45.0361" y1="55.4673" x2="67.3548" y2="55.4673" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<line x1="45.0361" y1="61.0054" x2="60.2339" y2="61.0054" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<rect x="45.0361" y="41.0605" width="27.0659" height="8.07692" rx="1.5" stroke="#DDDDDD" stroke-width="3"/>
<rect x="11.0156" y="18.1152" width="61.0879" height="14.4066" rx="1.5" stroke="#DDDDDD" stroke-width="3"/>
<path d="M58.5703 23.7363L61.4236 26.5897C61.8142 26.9802 62.4473 26.9802 62.8379 26.5897L65.6912 23.7363" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<line x1="17.5" y1="25" x2="39.8187" y2="25" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round"/>
<path d="M149.188 42.6308L134.977 30.1968" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<circle r="18.0392" transform="matrix(-1 0 0 1 122.541 19.5392)" fill="white" stroke="#DDDDDD" stroke-width="3" stroke-linejoin="round"/>
<path d="M121.297 15.834H131.667M121.297 24.0007H131.667" stroke="#DDDDDD" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse cx="114.333" cy="23.9993" rx="2.33333" ry="2.33333" fill="#DDDDDD"/>
<ellipse cx="114.333" cy="15.8333" rx="2.33333" ry="2.33333" fill="#DDDDDD"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,14 +0,0 @@
.woocommerce-attribute-empty-state {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
&__image {
max-width: 150px;
margin: $gap-larger 0 $gap-large;
}
&__add-new {
margin: $gap-large 0 $gap-larger;
}
}

View File

@ -1,58 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Card, CardBody } from '@wordpress/components';
import { Text } from '@woocommerce/experimental';
/**
* Internal dependencies
*/
import './attribute-empty-state.scss';
import AttributeEmptyStateLogo from './attribute-empty-state-logo.svg';
type AttributeEmptyStateProps = {
image?: string;
subtitle?: string;
addNewLabel?: string;
onNewClick?: () => void;
};
export const AttributeEmptyState: React.FC< AttributeEmptyStateProps > = ( {
image = AttributeEmptyStateLogo,
subtitle = __( 'No attributes yet', 'woocommerce' ),
addNewLabel = __( 'Add first attribute', 'woocommerce' ),
onNewClick,
} ) => {
return (
<Card>
<CardBody>
<div className="woocommerce-attribute-empty-state">
<img
src={ image }
alt="Completed"
className="woocommerce-attribute-empty-state__image"
/>
<Text
variant="subtitle.small"
weight="600"
size="14"
lineHeight="20px"
className="woocommerce-attribute-empty-state__subtitle"
>
{ subtitle }
</Text>
{ typeof onNewClick === 'function' && (
<Button
variant="secondary"
className="woocommerce-attribute-empty-state__add-new"
onClick={ onNewClick }
>
{ addNewLabel }
</Button>
) }
</div>
</CardBody>
</Card>
);
};

View File

@ -1,2 +0,0 @@
export * from './attribute-empty-state';
export { default as AttributeEmptyStateLogo } from './attribute-empty-state-logo.svg';

View File

@ -5,12 +5,12 @@ import { __ } from '@wordpress/i18n';
import { Product, ProductAttribute } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useFormContext } from '@woocommerce/components';
import { AttributeControl } from '@woocommerce/product-editor/src/components/attribute-control';
import { useProductAttributes } from '@woocommerce/product-editor/src/hooks/use-product-attributes';
/**
* Internal dependencies
*/
import { AttributeControl } from '../attribute-control';
import { useProductAttributes } from '~/products/hooks/use-product-attributes';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
type OptionsProps = {

View File

@ -2,13 +2,12 @@
* External dependencies
*/
import { useFormContext } from '@woocommerce/components';
import { __experimentalAttributes as Attributes } from '@woocommerce/product-editor';
import { Product } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { Attributes } from '../../fields/attributes';
export const AttributesField = () => {
const {

View File

@ -5,8 +5,8 @@ const { get } = require( 'lodash' );
const path = require( 'path' );
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
const CustomTemplatedPathPlugin = require( '@wordpress/custom-templated-path-webpack-plugin' );
const BundleAnalyzerPlugin =
require( 'webpack-bundle-analyzer' ).BundleAnalyzerPlugin;
const BundleAnalyzerPlugin = require( 'webpack-bundle-analyzer' )
.BundleAnalyzerPlugin;
const MomentTimezoneDataPlugin = require( 'moment-timezone-data-webpack-plugin' );
const ForkTsCheckerWebpackPlugin = require( 'fork-ts-checker-webpack-plugin' );
const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' );
@ -208,8 +208,8 @@ const webpackConfig = {
new CopyWebpackPlugin( {
patterns: [
{
from: '../../packages/js/product-editor/assets/icons',
to: './product-editor/icons',
from: '../../packages/js/product-editor/assets',
to: './product-editor',
},
],
} ),

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Moving product attributes components to product-editor package.

View File

@ -484,6 +484,26 @@ class WC_Post_Types {
),
),
),
array(
'woocommerce/product-section',
array(
'title' => __( 'Attributes', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: Attributes guide link opening tag. %2$s: Attributes guide link closing tag.*/
__( 'Add descriptive pieces of information that customers can use to filter and search for this product. %1$sLearn more%2$s', 'woocommerce' ),
'<a href="https://woocommerce.com/document/managing-product-taxonomies/#product-attributes" target="_blank" rel="noreferrer">',
'</a>'
),
'icon' => array(
'src' => plugins_url( '/assets/client/admin/product-editor/icons/section_attributes.svg', WC_PLUGIN_FILE ),
),
),
array(
array(
'woocommerce/product-attributes-field',
),
),
),
),
),
array(

View File

@ -2032,7 +2032,7 @@ importers:
specifier: workspace:*
version: link:../customer-effort-score
'@woocommerce/data':
specifier: workspace:^4.1.0
specifier: workspace:*
version: link:../data
'@woocommerce/experimental':
specifier: workspace:*
@ -2185,6 +2185,9 @@ importers:
'@types/wordpress__plugins':
specifier: ^3.0.0
version: 3.0.0(react-dom@17.0.2)(react@17.0.2)
'@types/wordpress__rich-text':
specifier: ^3.4.6
version: 3.4.6
'@woocommerce/eslint-plugin':
specifier: workspace:*
version: link:../eslint-plugin
@ -5976,8 +5979,8 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.12.9
'@babel/helper-annotate-as-pure': 7.16.7
'@babel/helper-create-class-features-plugin': 7.17.6(@babel/core@7.12.9)
'@babel/helper-annotate-as-pure': 7.18.6
'@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.12.9)
'@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.12.9)
transitivePeerDependencies:
@ -6006,8 +6009,8 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.3
'@babel/helper-annotate-as-pure': 7.16.7
'@babel/helper-create-class-features-plugin': 7.17.6(@babel/core@7.21.3)
'@babel/helper-annotate-as-pure': 7.18.6
'@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.21.3)
'@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.3)
transitivePeerDependencies:
@ -6827,7 +6830,7 @@ packages:
dependencies:
'@babel/core': 7.12.9
'@babel/helper-module-imports': 7.16.7
'@babel/helper-plugin-utils': 7.20.2
'@babel/helper-plugin-utils': 7.18.9
'@babel/helper-remap-async-to-generator': 7.16.8
transitivePeerDependencies:
- supports-color
@ -6855,7 +6858,7 @@ packages:
dependencies:
'@babel/core': 7.21.3
'@babel/helper-module-imports': 7.16.7
'@babel/helper-plugin-utils': 7.20.2
'@babel/helper-plugin-utils': 7.18.9
'@babel/helper-remap-async-to-generator': 7.16.8
transitivePeerDependencies:
- supports-color
@ -13468,7 +13471,7 @@ packages:
'@babel/generator': 7.17.7
'@babel/parser': 7.17.8
'@babel/plugin-transform-react-jsx': 7.17.3(@babel/core@7.17.8)
'@babel/preset-env': 7.16.11(@babel/core@7.17.8)
'@babel/preset-env': 7.20.2(@babel/core@7.17.8)
'@jest/transform': 26.6.2
'@mdx-js/loader': 1.6.22(react@17.0.2)
'@mdx-js/mdx': 1.6.22
@ -13492,7 +13495,7 @@ packages:
acorn: 7.4.1
acorn-jsx: 5.3.2(acorn@7.4.1)
acorn-walk: 7.2.0
core-js: 3.21.1
core-js: 3.29.1
doctrine: 3.0.0
escodegen: 2.0.0
fast-deep-equal: 3.1.3
@ -13508,7 +13511,7 @@ packages:
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
react-element-to-jsx-string: 14.3.4(react-dom@17.0.2)(react@17.0.2)
regenerator-runtime: 0.13.9
regenerator-runtime: 0.13.11
remark-external-links: 8.0.0
remark-slug: 6.1.0
ts-dedent: 2.2.0
@ -20836,8 +20839,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
dependencies:
browserslist: 4.20.2
caniuse-lite: 1.0.30001352
browserslist: 4.21.4
caniuse-lite: 1.0.30001418
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@ -22380,6 +22383,7 @@ packages:
escalade: 3.1.1
node-releases: 2.0.6
picocolors: 1.0.0
dev: true
/browserslist@4.20.4:
resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==}