Merge branch 'trunk' into update/contribution-guide-2023-03-03
This commit is contained in:
commit
7cde8d500e
|
@ -39,8 +39,8 @@ If you have questions about the process to contribute code or want to discuss de
|
|||
- Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file.
|
||||
- Push the changes to your fork and submit a pull request on the trunk branch of the WooCommerce repository.
|
||||
- Make sure to write good and detailed commit messages (see [this post](https://chris.beams.io/posts/git-commit/) for more on this) and follow all the applicable sections of the pull request template.
|
||||
- Please add a changelog entry by following the steps detailed in the [development guide](https://github.com/woocommerce/woocommerce/blob/trunk/DEVELOPMENT.md), but do not modify `changelog.txt` directly.
|
||||
- Please do not update any `.pot` files. These will be updated by the WooCommerce team.
|
||||
- Please create a change file for your changes by running `pnpm --filter=<project> changelog add`. For example, a change file for the WooCommerce Core project would be added by running `pnpm --filter=woocommerce changelog add`.
|
||||
- Please avoid modifying the changelog directly or updating the .pot files. These will be updated by the WooCommerce team.
|
||||
|
||||
If you are contributing code to our (Javascript-driven) Gutenberg blocks, please note that they are developed in their [own repository](https://github.com/woocommerce/woocommerce-gutenberg-products-block) and have their [own issue tracker](https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues).
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Add unit tests
|
||||
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import React, { createElement } from '@wordpress/element';
|
||||
import { SelectTree } from '../select-tree';
|
||||
import { Item } from '../../experimental-tree-control';
|
||||
|
||||
const mockItems: Item[] = [
|
||||
{
|
||||
label: 'Item 1',
|
||||
value: 'item-1',
|
||||
},
|
||||
{
|
||||
label: 'Item 2',
|
||||
value: 'item-2',
|
||||
parent: 'item-1',
|
||||
},
|
||||
{
|
||||
label: 'Item 3',
|
||||
value: 'item-3',
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
id: 'select-tree',
|
||||
items: mockItems,
|
||||
label: 'Select Tree',
|
||||
placeholder: 'Type here',
|
||||
};
|
||||
|
||||
describe( 'SelectTree', () => {
|
||||
beforeEach( () => {
|
||||
jest.clearAllMocks();
|
||||
} );
|
||||
|
||||
it( 'should show the popover only when focused', () => {
|
||||
const { queryByPlaceholderText, queryByText } = render(
|
||||
<SelectTree { ...DEFAULT_PROPS } />
|
||||
);
|
||||
expect( queryByText( 'Item 1' ) ).not.toBeInTheDocument();
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should show create button when callback is true ', () => {
|
||||
const { queryByText, queryByPlaceholderText } = render(
|
||||
<SelectTree
|
||||
{ ...DEFAULT_PROPS }
|
||||
shouldShowCreateButton={ () => true }
|
||||
/>
|
||||
);
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
expect( queryByText( 'Create new' ) ).toBeInTheDocument();
|
||||
} );
|
||||
it( 'should not show create button when callback is false or no callback', () => {
|
||||
const { queryByText, queryByPlaceholderText } = render(
|
||||
<SelectTree { ...DEFAULT_PROPS } />
|
||||
);
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
expect( queryByText( 'Create new' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
it( 'should show a root item when focused and child when expand button is clicked', () => {
|
||||
const { queryByText, queryByLabelText, queryByPlaceholderText } =
|
||||
render( <SelectTree { ...DEFAULT_PROPS } /> );
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
expect( queryByText( 'Item 1' ) ).toBeInTheDocument();
|
||||
|
||||
expect( queryByText( 'Item 2' ) ).not.toBeInTheDocument();
|
||||
queryByLabelText( 'Expand' )?.click();
|
||||
expect( queryByText( 'Item 2' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should show selected items', () => {
|
||||
const { queryAllByRole, queryByPlaceholderText } = render(
|
||||
<SelectTree { ...DEFAULT_PROPS } selected={ [ mockItems[ 0 ] ] } />
|
||||
);
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
expect( queryAllByRole( 'treeitem' )[ 0 ] ).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should show Create "<createValue>" button', () => {
|
||||
const { queryByPlaceholderText, queryByText } = render(
|
||||
<SelectTree
|
||||
{ ...DEFAULT_PROPS }
|
||||
createValue="new item"
|
||||
shouldShowCreateButton={ () => true }
|
||||
/>
|
||||
);
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
expect( queryByText( 'Create "new item"' ) ).toBeInTheDocument();
|
||||
} );
|
||||
it( 'should call onCreateNew when Create "<createValue>" button is clicked', () => {
|
||||
const mockFn = jest.fn();
|
||||
const { queryByPlaceholderText, queryByText } = render(
|
||||
<SelectTree
|
||||
{ ...DEFAULT_PROPS }
|
||||
createValue="new item"
|
||||
shouldShowCreateButton={ () => true }
|
||||
onCreateNew={ mockFn }
|
||||
/>
|
||||
);
|
||||
queryByPlaceholderText( 'Type here' )?.focus();
|
||||
queryByText( 'Create "new item"' )?.click();
|
||||
expect( mockFn ).toBeCalledTimes( 1 );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add action buttons to the editor header
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add product radio block and tax class to product blocks editor
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Prevent click event when the element is aria-disabled"
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Use SelectTree component in product editor's category field
|
|
@ -8,7 +8,8 @@
|
|||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"toggleText": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"initialCollapsed": {
|
||||
"type": "boolean"
|
||||
|
|
|
@ -9,6 +9,11 @@ import { ProductCategory } from '@woocommerce/data';
|
|||
import { __experimentalSelectControlMenuItemProps as MenuItemProps } from '@woocommerce/components';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductCategoryNode } from './use-category-search';
|
||||
|
||||
export type CategoryTreeItem = {
|
||||
data: ProductCategory;
|
||||
children: CategoryTreeItem[];
|
||||
|
@ -19,13 +24,10 @@ export type CategoryTreeItem = {
|
|||
type CategoryFieldItemProps = {
|
||||
item: CategoryTreeItem;
|
||||
selectedIds: number[];
|
||||
items: Pick< ProductCategory, 'id' | 'name' >[];
|
||||
items: ProductCategoryNode[];
|
||||
highlightedIndex: number;
|
||||
openParent?: () => void;
|
||||
} & Pick<
|
||||
MenuItemProps< Pick< ProductCategory, 'id' | 'name' > >,
|
||||
'getItemProps'
|
||||
>;
|
||||
} & Pick< MenuItemProps< ProductCategoryNode >, 'getItemProps' >;
|
||||
|
||||
export const CategoryFieldItem: React.FC< CategoryFieldItemProps > = ( {
|
||||
item,
|
||||
|
|
|
@ -3,11 +3,8 @@
|
|||
*/
|
||||
import { useMemo, useState, createElement, Fragment } from '@wordpress/element';
|
||||
import {
|
||||
selectControlStateChangeTypes,
|
||||
Spinner,
|
||||
__experimentalSelectControl as SelectControl,
|
||||
__experimentalSelectControlMenuSlot as MenuSlot,
|
||||
__experimentalSelectControlMenu as Menu,
|
||||
TreeItemType,
|
||||
__experimentalSelectTreeControl as SelectTree,
|
||||
} from '@woocommerce/components';
|
||||
import { ProductCategory } from '@woocommerce/data';
|
||||
import { debounce } from 'lodash';
|
||||
|
@ -15,16 +12,15 @@ import { debounce } from 'lodash';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryFieldItem, CategoryTreeItem } from './category-field-item';
|
||||
import { useCategorySearch } from './use-category-search';
|
||||
import { CategoryTreeItem } from './category-field-item';
|
||||
import { useCategorySearch, ProductCategoryNode } from './use-category-search';
|
||||
import { CreateCategoryModal } from './create-category-modal';
|
||||
import { CategoryFieldAddNewItem } from './category-field-add-new-item';
|
||||
|
||||
type CategoryFieldProps = {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value?: Pick< ProductCategory, 'id' | 'name' >[];
|
||||
onChange: ( value: Pick< ProductCategory, 'id' | 'name' >[] ) => void;
|
||||
value?: ProductCategoryNode[];
|
||||
onChange: ( value: ProductCategoryNode[] ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -32,11 +28,11 @@ type CategoryFieldProps = {
|
|||
* if not included already.
|
||||
*/
|
||||
function getSelectedWithParents(
|
||||
selected: Pick< ProductCategory, 'id' | 'name' >[] = [],
|
||||
selected: ProductCategoryNode[] = [],
|
||||
item: ProductCategory,
|
||||
treeKeyValues: Record< number, CategoryTreeItem >
|
||||
): Pick< ProductCategory, 'id' | 'name' >[] {
|
||||
selected.push( { id: item.id, name: item.name } );
|
||||
): ProductCategoryNode[] {
|
||||
selected.push( { id: item.id, name: item.name, parent: item.parent } );
|
||||
|
||||
const parentId =
|
||||
item.parent !== undefined
|
||||
|
@ -59,6 +55,33 @@ function getSelectedWithParents(
|
|||
return selected;
|
||||
}
|
||||
|
||||
function mapFromCategoryType(
|
||||
categories: ProductCategoryNode[]
|
||||
): TreeItemType[] {
|
||||
return categories.map( ( val ) =>
|
||||
val.parent
|
||||
? {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
parent: String( val.parent ),
|
||||
}
|
||||
: {
|
||||
value: String( val.id ),
|
||||
label: val.name,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function mapToCategoryType(
|
||||
categories: TreeItemType[]
|
||||
): ProductCategoryNode[] {
|
||||
return categories.map( ( cat ) => ( {
|
||||
id: +cat.value,
|
||||
name: cat.label,
|
||||
parent: cat.parent ? +cat.parent : 0,
|
||||
} ) );
|
||||
}
|
||||
|
||||
export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
||||
label,
|
||||
placeholder,
|
||||
|
@ -70,7 +93,7 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
categoriesSelectList,
|
||||
categoryTreeKeyValues,
|
||||
searchCategories,
|
||||
getFilteredItems,
|
||||
getFilteredItemsForSelectTree,
|
||||
} = useCategorySearch();
|
||||
const [ showCreateNewModal, setShowCreateNewModal ] = useState( false );
|
||||
const [ searchValue, setSearchValue ] = useState( '' );
|
||||
|
@ -85,167 +108,62 @@ export const CategoryField: React.FC< CategoryFieldProps > = ( {
|
|||
[ onInputChange ]
|
||||
);
|
||||
|
||||
const onSelect = ( itemId: number, selected: boolean ) => {
|
||||
if ( itemId === -99 ) {
|
||||
setShowCreateNewModal( true );
|
||||
return;
|
||||
}
|
||||
if ( selected ) {
|
||||
const item = categoryTreeKeyValues[ itemId ].data;
|
||||
if ( item ) {
|
||||
onChange(
|
||||
getSelectedWithParents(
|
||||
[ ...value ],
|
||||
item,
|
||||
categoryTreeKeyValues
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onChange( value.filter( ( i ) => i.id !== itemId ) );
|
||||
}
|
||||
};
|
||||
|
||||
const categoryFieldGetFilteredItems = (
|
||||
allItems: Pick< ProductCategory, 'id' | 'name' >[],
|
||||
inputValue: string,
|
||||
selectedItems: Pick< ProductCategory, 'id' | 'name' >[]
|
||||
) => {
|
||||
const filteredItems = getFilteredItems(
|
||||
allItems,
|
||||
inputValue,
|
||||
selectedItems
|
||||
);
|
||||
if (
|
||||
inputValue.length > 0 &&
|
||||
! isSearching &&
|
||||
! filteredItems.find(
|
||||
( item ) => item.name.toLowerCase() === inputValue.toLowerCase()
|
||||
)
|
||||
) {
|
||||
return [
|
||||
...filteredItems,
|
||||
{
|
||||
id: -99,
|
||||
name: inputValue,
|
||||
},
|
||||
];
|
||||
}
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
const selectedIds = value.map( ( item ) => item.id );
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectControl< Pick< ProductCategory, 'id' | 'name' > >
|
||||
className="woocommerce-category-field-dropdown components-base-control"
|
||||
<SelectTree
|
||||
id="category-field"
|
||||
multiple
|
||||
items={ categoriesSelectList }
|
||||
shouldNotRecursivelySelect
|
||||
createValue={ searchValue }
|
||||
label={ label }
|
||||
selected={ value }
|
||||
getItemLabel={ ( item ) => item?.name || '' }
|
||||
getItemValue={ ( item ) => item?.id || '' }
|
||||
onSelect={ ( item ) => {
|
||||
if ( item ) {
|
||||
onSelect( item.id, ! selectedIds.includes( item.id ) );
|
||||
}
|
||||
} }
|
||||
onRemove={ ( item ) => item && onSelect( item.id, false ) }
|
||||
isLoading={ isSearching }
|
||||
onInputChange={ searchDelayed }
|
||||
getFilteredItems={ categoryFieldGetFilteredItems }
|
||||
placeholder={ value.length === 0 ? placeholder : '' }
|
||||
stateReducer={ ( state, actionAndChanges ) => {
|
||||
const { changes, type } = actionAndChanges;
|
||||
switch ( type ) {
|
||||
case selectControlStateChangeTypes.ControlledPropUpdatedSelectedItem:
|
||||
return {
|
||||
...changes,
|
||||
inputValue: state.inputValue,
|
||||
};
|
||||
case selectControlStateChangeTypes.ItemClick:
|
||||
if (
|
||||
changes.selectedItem &&
|
||||
changes.selectedItem.id === -99
|
||||
) {
|
||||
return changes;
|
||||
}
|
||||
return {
|
||||
...changes,
|
||||
isOpen: true,
|
||||
inputValue: state.inputValue,
|
||||
highlightedIndex: state.highlightedIndex,
|
||||
};
|
||||
default:
|
||||
return changes;
|
||||
onCreateNew={ () => {
|
||||
setShowCreateNewModal( true );
|
||||
} }
|
||||
shouldShowCreateButton={ ( typedValue ) =>
|
||||
! typedValue ||
|
||||
categoriesSelectList.findIndex(
|
||||
( item ) => item.name === typedValue
|
||||
) === -1
|
||||
}
|
||||
items={ getFilteredItemsForSelectTree(
|
||||
mapFromCategoryType( categoriesSelectList ),
|
||||
searchValue,
|
||||
mapFromCategoryType( value )
|
||||
) }
|
||||
selected={ mapFromCategoryType( value ) }
|
||||
onSelect={ ( selectedItems ) => {
|
||||
if ( Array.isArray( selectedItems ) ) {
|
||||
const newItems: ProductCategoryNode[] =
|
||||
mapToCategoryType(
|
||||
selectedItems.filter(
|
||||
( { value: selectedItemValue } ) =>
|
||||
! value.some(
|
||||
( item ) =>
|
||||
item.id === +selectedItemValue
|
||||
)
|
||||
)
|
||||
);
|
||||
onChange( [ ...value, ...newItems ] );
|
||||
}
|
||||
} }
|
||||
__experimentalOpenMenuOnFocus
|
||||
>
|
||||
{ ( {
|
||||
items,
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
} ) => {
|
||||
const rootItems =
|
||||
items.length > 0
|
||||
? items.filter(
|
||||
( item ) =>
|
||||
categoryTreeKeyValues[ item.id ]
|
||||
?.parentID === 0 || item.id === -99
|
||||
)
|
||||
: [];
|
||||
return (
|
||||
<Menu
|
||||
isOpen={ isOpen }
|
||||
getMenuProps={ getMenuProps }
|
||||
className="woocommerce-category-field-dropdown__menu"
|
||||
>
|
||||
<>
|
||||
{ isSearching ? (
|
||||
<li className="woocommerce-category-field-dropdown__item">
|
||||
<div className="woocommerce-category-field-dropdown__item-content">
|
||||
<Spinner />
|
||||
</div>
|
||||
</li>
|
||||
) : (
|
||||
rootItems.map( ( item ) => {
|
||||
return item.id === -99 ? (
|
||||
<CategoryFieldAddNewItem
|
||||
key={ `${ item.id }` }
|
||||
item={ item }
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
items={ items }
|
||||
getItemProps={ getItemProps }
|
||||
/>
|
||||
) : (
|
||||
<CategoryFieldItem
|
||||
key={ `${ item.id }` }
|
||||
item={
|
||||
categoryTreeKeyValues[
|
||||
item.id
|
||||
]
|
||||
}
|
||||
highlightedIndex={
|
||||
highlightedIndex
|
||||
}
|
||||
selectedIds={ selectedIds }
|
||||
items={ items }
|
||||
getItemProps={ getItemProps }
|
||||
/>
|
||||
);
|
||||
} )
|
||||
) }
|
||||
</>
|
||||
</Menu>
|
||||
);
|
||||
onRemove={ ( removedItems ) => {
|
||||
const newValues = Array.isArray( removedItems )
|
||||
? value.filter(
|
||||
( item ) =>
|
||||
! removedItems.some(
|
||||
( { value: removedValue } ) =>
|
||||
item.id === +removedValue
|
||||
)
|
||||
)
|
||||
: value.filter(
|
||||
( item ) => item.id !== +removedItems.value
|
||||
);
|
||||
onChange( newValues );
|
||||
} }
|
||||
</SelectControl>
|
||||
<MenuSlot />
|
||||
></SelectTree>
|
||||
{ showCreateNewModal && (
|
||||
<CreateCategoryModal
|
||||
initialCategoryName={ searchValue }
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCategorySearch } from './use-category-search';
|
||||
import { ProductCategoryNode, useCategorySearch } from './use-category-search';
|
||||
import { CategoryFieldItem } from './category-field-item';
|
||||
|
||||
type CreateCategoryModalProps = {
|
||||
|
@ -51,10 +51,8 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
|||
const [ categoryName, setCategoryName ] = useState(
|
||||
initialCategoryName || ''
|
||||
);
|
||||
const [ categoryParent, setCategoryParent ] = useState< Pick<
|
||||
ProductCategory,
|
||||
'id' | 'name'
|
||||
> | null >( null );
|
||||
const [ categoryParent, setCategoryParent ] =
|
||||
useState< ProductCategoryNode | null >( null );
|
||||
|
||||
const onSave = async () => {
|
||||
recordEvent( 'product_category_add', {
|
||||
|
@ -94,7 +92,7 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( {
|
|||
value={ categoryName }
|
||||
onChange={ setCategoryName }
|
||||
/>
|
||||
<SelectControl< Pick< ProductCategory, 'id' | 'name' > >
|
||||
<SelectControl< ProductCategoryNode >
|
||||
items={ categoriesSelectList }
|
||||
label={ createInterpolateElement(
|
||||
__( 'Parent category <optional/>', 'woocommerce' ),
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { Product, ProductCategory } from '@woocommerce/data';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryField } from './category-field';
|
||||
import { ProductCategoryNode } from './use-category-search';
|
||||
|
||||
export const DetailsCategoriesField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
|
@ -18,9 +19,7 @@ export const DetailsCategoriesField = () => {
|
|||
<CategoryField
|
||||
label={ __( 'Categories', 'woocommerce' ) }
|
||||
placeholder={ __( 'Search or create category…', 'woocommerce' ) }
|
||||
{ ...getInputProps< Pick< ProductCategory, 'id' | 'name' >[] >(
|
||||
'categories'
|
||||
) }
|
||||
{ ...getInputProps< ProductCategoryNode[] >( 'categories' ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,94 +1,17 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ReactElement, Component } from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Form, FormContextType } from '@woocommerce/components';
|
||||
import { Product, ProductCategory } from '@woocommerce/data';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CategoryField } from '../category-field';
|
||||
import {
|
||||
getCategoriesTreeWithMissingParents,
|
||||
useCategorySearch,
|
||||
} from '../use-category-search';
|
||||
import { ProductCategoryNode } from '../use-category-search';
|
||||
|
||||
const mockCategoryList = [
|
||||
{ id: 1, name: 'Clothing', parent: 0 },
|
||||
{ id: 2, name: 'Hoodies', parent: 1 },
|
||||
{ id: 3, name: 'Rain gear', parent: 0 },
|
||||
] as ProductCategory[];
|
||||
|
||||
jest.mock( '@woocommerce/components', () => {
|
||||
const originalModule = jest.requireActual( '@woocommerce/components' );
|
||||
|
||||
type ChildrenProps = {
|
||||
items: ProductCategory[];
|
||||
isOpen: boolean;
|
||||
highlightedIndex: number;
|
||||
getMenuProps: () => Record< string, string >;
|
||||
getItemProps: () => Record< string, string >;
|
||||
selectItem: ( item: ProductCategory ) => void;
|
||||
setInputValue: ( value: string ) => void;
|
||||
};
|
||||
type SelectControlProps = {
|
||||
children: ( {}: ChildrenProps ) => ReactElement | Component;
|
||||
items: ProductCategory[];
|
||||
label: string;
|
||||
initialSelectedItems?: ProductCategory[];
|
||||
itemToString?: ( item: ProductCategory | null ) => string;
|
||||
getFilteredItems?: (
|
||||
allItems: ProductCategory[],
|
||||
inputValue: string,
|
||||
selectedItems: ProductCategory[]
|
||||
) => ProductCategory[];
|
||||
multiple?: boolean;
|
||||
onInputChange?: ( value: string | undefined ) => void;
|
||||
onRemove?: ( item: ProductCategory ) => void;
|
||||
onSelect?: ( selected: ProductCategory ) => void;
|
||||
placeholder?: string;
|
||||
selected: ProductCategory[];
|
||||
};
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
__experimentalSelectControlMenu: ( {
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
} ) => children,
|
||||
__experimentalSelectControl: ( {
|
||||
children,
|
||||
items,
|
||||
selected,
|
||||
}: SelectControlProps ) => {
|
||||
return (
|
||||
<div>
|
||||
[select-control]
|
||||
<div className="selected">
|
||||
{ selected.map( ( item ) => (
|
||||
<div key={ item.id }>{ item.name }</div>
|
||||
) ) }
|
||||
</div>
|
||||
<div className="children">
|
||||
{ children( {
|
||||
items,
|
||||
isOpen: true,
|
||||
getMenuProps: () => ( {} ),
|
||||
selectItem: () => {},
|
||||
highlightedIndex: -1,
|
||||
setInputValue: () => {},
|
||||
getItemProps: () => ( {} ),
|
||||
} ) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
} );
|
||||
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
|
||||
|
||||
jest.mock( '../use-category-search', () => {
|
||||
|
@ -98,7 +21,7 @@ jest.mock( '../use-category-search', () => {
|
|||
originalModule.getCategoriesTreeWithMissingParents,
|
||||
useCategorySearch: jest.fn().mockReturnValue( {
|
||||
searchCategories: jest.fn(),
|
||||
getFilteredItems: jest.fn(),
|
||||
getFilteredItemsForSelectTree: jest.fn().mockReturnValue( [] ),
|
||||
isSearching: false,
|
||||
categoriesSelectList: [],
|
||||
categoryTreeKeyValues: {},
|
||||
|
@ -112,24 +35,25 @@ describe( 'CategoryField', () => {
|
|||
} );
|
||||
|
||||
it( 'should render a dropdown select control', () => {
|
||||
const { queryByText } = render(
|
||||
const { queryByText, queryByPlaceholderText } = render(
|
||||
<Form initialValues={ { categories: [] } }>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
{ ...getInputProps< ProductCategoryNode[] >(
|
||||
'categories'
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
expect( queryByText( '[select-control]' ) ).toBeInTheDocument();
|
||||
queryByPlaceholderText( 'Search or create category…' )?.focus();
|
||||
expect( queryByText( 'Create new' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should pass in the selected categories as select control items', () => {
|
||||
const { queryByText } = render(
|
||||
const { queryAllByText, queryByPlaceholderText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [
|
||||
|
@ -142,189 +66,15 @@ describe( 'CategoryField', () => {
|
|||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
{ ...getInputProps< ProductCategoryNode[] >(
|
||||
'categories'
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
expect( queryByText( 'Test' ) ).toBeInTheDocument();
|
||||
expect( queryByText( 'Clothing' ) ).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
describe( 'search values', () => {
|
||||
beforeEach( async () => {
|
||||
const items = await getCategoriesTreeWithMissingParents(
|
||||
mockCategoryList,
|
||||
''
|
||||
);
|
||||
( useCategorySearch as jest.Mock ).mockReturnValue( {
|
||||
searchCategories: jest.fn(),
|
||||
getFilteredItems: jest.fn(),
|
||||
isSearching: false,
|
||||
categoriesSelectList: items[ 0 ],
|
||||
categoryTreeKeyValues: items[ 2 ],
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should display only the parent categories passed in to the categoriesSelectList', () => {
|
||||
const { queryByText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [],
|
||||
} }
|
||||
>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
expect(
|
||||
queryByText( mockCategoryList[ 0 ].name )
|
||||
).toBeInTheDocument();
|
||||
const childParent = queryByText(
|
||||
mockCategoryList[ 1 ].name
|
||||
)?.parentElement?.closest(
|
||||
'.woocommerce-category-field-dropdown__item-children'
|
||||
);
|
||||
expect( childParent ).toBeInTheDocument();
|
||||
expect( childParent?.className ).not.toMatch(
|
||||
'woocommerce-category-field-dropdown__item-open'
|
||||
);
|
||||
expect(
|
||||
queryByText( mockCategoryList[ 2 ].name )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should show selected categories as selected', () => {
|
||||
const { getByLabelText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [ mockCategoryList[ 2 ] ],
|
||||
} }
|
||||
>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
const rainGearCheckbox = getByLabelText(
|
||||
mockCategoryList[ 2 ].name
|
||||
);
|
||||
expect( rainGearCheckbox ).toBeChecked();
|
||||
const clothingCheckbox = getByLabelText(
|
||||
mockCategoryList[ 0 ].name
|
||||
);
|
||||
expect( clothingCheckbox ).not.toBeChecked();
|
||||
} );
|
||||
|
||||
it( 'should show selected categories as selected', () => {
|
||||
const { getByLabelText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [ mockCategoryList[ 2 ] ],
|
||||
} }
|
||||
>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
const rainGearCheckbox = getByLabelText(
|
||||
mockCategoryList[ 2 ].name
|
||||
);
|
||||
expect( rainGearCheckbox ).toBeChecked();
|
||||
const clothingCheckbox = getByLabelText(
|
||||
mockCategoryList[ 0 ].name
|
||||
);
|
||||
expect( clothingCheckbox ).not.toBeChecked();
|
||||
} );
|
||||
|
||||
it( 'should include a toggle icon for parents that contain children', () => {
|
||||
const { getByLabelText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [ mockCategoryList[ 2 ] ],
|
||||
} }
|
||||
>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
const rainGearCheckboxParent = getByLabelText(
|
||||
mockCategoryList[ 0 ].name
|
||||
).parentElement?.closest(
|
||||
'.woocommerce-category-field-dropdown__item-content'
|
||||
);
|
||||
|
||||
expect(
|
||||
rainGearCheckboxParent?.querySelector( 'svg' )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should allow user to toggle the parents using the svg button', () => {
|
||||
const { getByLabelText, queryByText } = render(
|
||||
<Form
|
||||
initialValues={ {
|
||||
categories: [ mockCategoryList[ 2 ] ],
|
||||
} }
|
||||
>
|
||||
{ ( { getInputProps }: FormContextType< Product > ) => (
|
||||
<CategoryField
|
||||
label="Categories"
|
||||
placeholder="Search or create category…"
|
||||
{ ...getInputProps<
|
||||
Pick< ProductCategory, 'id' | 'name' >[]
|
||||
>( 'categories' ) }
|
||||
/>
|
||||
) }
|
||||
</Form>
|
||||
);
|
||||
const rainGearCheckboxParent = getByLabelText(
|
||||
mockCategoryList[ 0 ].name
|
||||
).parentElement?.closest(
|
||||
'.woocommerce-category-field-dropdown__item-content'
|
||||
);
|
||||
|
||||
const toggle = rainGearCheckboxParent?.querySelector( 'svg' );
|
||||
if ( toggle ) {
|
||||
fireEvent.click( toggle );
|
||||
}
|
||||
const childParent = queryByText(
|
||||
mockCategoryList[ 1 ].name
|
||||
)?.parentElement?.closest(
|
||||
'.woocommerce-category-field-dropdown__item-children'
|
||||
);
|
||||
expect( childParent ).toBeInTheDocument();
|
||||
expect( childParent?.className ).toMatch(
|
||||
'woocommerce-category-field-dropdown__item-open'
|
||||
);
|
||||
} );
|
||||
queryByPlaceholderText( 'Search or create category…' )?.focus();
|
||||
expect( queryAllByText( 'Test' ) ).toHaveLength( 2 );
|
||||
expect( queryAllByText( 'Clothing' ) ).toHaveLength( 2 );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
ProductCategory,
|
||||
} from '@woocommerce/data';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
import { TreeItemType } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -33,6 +34,11 @@ function openParents(
|
|||
}
|
||||
}
|
||||
|
||||
export type ProductCategoryNode = Pick<
|
||||
ProductCategory,
|
||||
'id' | 'name' | 'parent'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Sort function for category tree items, sorts by popularity and then alphabetically.
|
||||
*/
|
||||
|
@ -256,9 +262,9 @@ export const useCategorySearch = () => {
|
|||
*/
|
||||
const getFilteredItems = useCallback(
|
||||
(
|
||||
allItems: Pick< ProductCategory, 'id' | 'name' >[],
|
||||
allItems: ProductCategoryNode[],
|
||||
inputValue: string,
|
||||
selectedItems: Pick< ProductCategory, 'id' | 'name' >[]
|
||||
selectedItems: ProductCategoryNode[]
|
||||
) => {
|
||||
const searchRegex = new RegExp( escapeRegExp( inputValue ), 'i' );
|
||||
return allItems.filter(
|
||||
|
@ -272,9 +278,33 @@ export const useCategorySearch = () => {
|
|||
[ categoriesAndNewItem ]
|
||||
);
|
||||
|
||||
/**
|
||||
* this is the same as getFilteredItems but for the SelectTree component, where item id is a string.
|
||||
* After all the occurrences of getFilteredItems are migrated to use SelectTree,
|
||||
* this can become the standard version
|
||||
*/
|
||||
const getFilteredItemsForSelectTree = useCallback(
|
||||
(
|
||||
allItems: TreeItemType[],
|
||||
inputValue: string,
|
||||
selectedItems: TreeItemType[]
|
||||
) => {
|
||||
const searchRegex = new RegExp( escapeRegExp( inputValue ), 'i' );
|
||||
return allItems.filter(
|
||||
( item ) =>
|
||||
selectedItems.indexOf( item ) < 0 &&
|
||||
( searchRegex.test( item.label ) ||
|
||||
( categoryTreeKeyValues[ +item.value ] &&
|
||||
categoryTreeKeyValues[ +item.value ].isOpen ) )
|
||||
);
|
||||
},
|
||||
[ categoriesAndNewItem ]
|
||||
);
|
||||
|
||||
return {
|
||||
searchCategories,
|
||||
getFilteredItems,
|
||||
getFilteredItemsForSelectTree,
|
||||
categoriesSelectList: categoriesAndNewItem[ 0 ],
|
||||
categories: categoriesAndNewItem[ 1 ],
|
||||
isSearching,
|
||||
|
|
|
@ -48,12 +48,7 @@ export function Editor( { product, settings }: EditorProps ) {
|
|||
<FullscreenMode isActive={ false } />
|
||||
<SlotFillProvider>
|
||||
<InterfaceSkeleton
|
||||
header={
|
||||
<Header
|
||||
productId={ product.id }
|
||||
productName={ product.name }
|
||||
/>
|
||||
}
|
||||
header={ <Header productName={ product.name } /> }
|
||||
content={
|
||||
<>
|
||||
<BlockEditor
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
*/
|
||||
import { init as initImages } from '../images';
|
||||
import { init as initName } from '../details-name-block';
|
||||
import { init as initRadio } from '../radio';
|
||||
import { init as initSummary } from '../details-summary-block';
|
||||
import { init as initSection } from '../section';
|
||||
import { init as initTab } from '../tab';
|
||||
|
@ -31,6 +32,7 @@ export const initBlocks = () => {
|
|||
|
||||
initImages();
|
||||
initName();
|
||||
initRadio();
|
||||
initSummary();
|
||||
initSection();
|
||||
initTab();
|
||||
|
|
|
@ -1,74 +1,31 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { navigateTo, getNewPath } from '@woocommerce/navigation';
|
||||
import { WooHeaderItem } from '@woocommerce/admin-layout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AUTO_DRAFT_NAME, getHeaderTitle } from '../../utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getHeaderTitle } from '../../utils';
|
||||
import { MoreMenu } from './more-menu';
|
||||
import { PreviewButton } from './preview-button';
|
||||
import { SaveDraftButton } from './save-draft-button';
|
||||
import { PublishButton } from './publish-button';
|
||||
|
||||
export type HeaderProps = {
|
||||
productId: number;
|
||||
productName: string;
|
||||
};
|
||||
|
||||
export function Header( { productId, productName }: HeaderProps ) {
|
||||
const { isProductLocked, isSaving, editedProductName } = useSelect(
|
||||
( select ) => {
|
||||
const { isSavingEntityRecord, getEditedEntityRecord } =
|
||||
select( 'core' );
|
||||
const { isPostSavingLocked } = select( 'core/editor' );
|
||||
|
||||
const product: Product = getEditedEntityRecord(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
return {
|
||||
isProductLocked: isPostSavingLocked(),
|
||||
isSaving: isSavingEntityRecord(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
),
|
||||
editedProductName: product?.name,
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
export function Header( { productName }: HeaderProps ) {
|
||||
const [ editedProductName ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'name'
|
||||
);
|
||||
|
||||
const isDisabled = isProductLocked || isSaving;
|
||||
const isCreating = productName === AUTO_DRAFT_NAME;
|
||||
|
||||
const { saveEditedEntityRecord } = useDispatch( 'core' );
|
||||
|
||||
function handleSave() {
|
||||
saveEditedEntityRecord< Product >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
).then( ( response ) => {
|
||||
if ( isCreating ) {
|
||||
navigateTo( {
|
||||
url: getNewPath( {}, `/product/${ response.id }` ),
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="woocommerce-product-header"
|
||||
|
@ -81,16 +38,12 @@ export function Header( { productId, productName }: HeaderProps ) {
|
|||
</h1>
|
||||
|
||||
<div className="woocommerce-product-header__actions">
|
||||
<Button
|
||||
onClick={ handleSave }
|
||||
variant="primary"
|
||||
isBusy={ isSaving }
|
||||
disabled={ isDisabled }
|
||||
>
|
||||
{ isCreating
|
||||
? __( 'Add', 'woocommerce' )
|
||||
: __( 'Save', 'woocommerce' ) }
|
||||
</Button>
|
||||
<SaveDraftButton />
|
||||
|
||||
<PreviewButton />
|
||||
|
||||
<PublishButton />
|
||||
|
||||
<WooHeaderItem.Slot name="product" />
|
||||
<MoreMenu />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './use-preview';
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product, ProductStatus } from '@woocommerce/data';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useRef } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
export function usePreview( {
|
||||
disabled,
|
||||
onClick,
|
||||
onSaveSuccess,
|
||||
onSaveError,
|
||||
...props
|
||||
}: Omit< Button.AnchorProps, 'aria-disabled' | 'variant' | 'href' > & {
|
||||
onSaveSuccess?( product: Product ): void;
|
||||
onSaveError?( error: Error ): void;
|
||||
} ): Button.AnchorProps {
|
||||
const anchorRef = useRef< HTMLAnchorElement >();
|
||||
|
||||
const [ productId ] = useEntityProp< number >(
|
||||
'postType',
|
||||
'product',
|
||||
'id'
|
||||
);
|
||||
const [ productStatus ] = useEntityProp< ProductStatus | 'auto-draft' >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
const [ permalink ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'permalink'
|
||||
);
|
||||
|
||||
const { hasEdits, isDisabled } = useSelect(
|
||||
( select ) => {
|
||||
const { hasEditsForEntityRecord, isSavingEntityRecord } =
|
||||
select( 'core' );
|
||||
const { isPostSavingLocked } = select( 'core/editor' );
|
||||
const isSavingLocked = isPostSavingLocked();
|
||||
const isSaving = isSavingEntityRecord< boolean >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
return {
|
||||
isDisabled: isSavingLocked || isSaving,
|
||||
hasEdits: hasEditsForEntityRecord< boolean >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const ariaDisabled = disabled || isDisabled;
|
||||
|
||||
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
|
||||
|
||||
let previewLink: URL | undefined;
|
||||
if ( typeof permalink === 'string' ) {
|
||||
previewLink = new URL( permalink );
|
||||
previewLink.searchParams.append( 'preview', 'true' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default anchor behaviour when the product has unsaved changes.
|
||||
* Before redirecting to the preview page all changes are saved and then the
|
||||
* redirection is performed.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
async function handleClick( event: MouseEvent< HTMLAnchorElement > ) {
|
||||
if ( ariaDisabled ) {
|
||||
return event.preventDefault();
|
||||
}
|
||||
|
||||
if ( onClick ) {
|
||||
onClick( event );
|
||||
}
|
||||
|
||||
// Prevent an infinite recursion call due to the
|
||||
// `anchorRef.current?.click()` call.
|
||||
if ( ! hasEdits ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent the default anchor behaviour.
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
// If the product status is `auto-draft` it's not possible to
|
||||
// reach the preview page, so the status is changed to `draft`
|
||||
// before redirecting.
|
||||
if ( productStatus === 'auto-draft' ) {
|
||||
await editEntityRecord( 'postType', 'product', productId, {
|
||||
status: 'draft',
|
||||
} );
|
||||
}
|
||||
|
||||
// Persist the product changes before redirecting
|
||||
const publishedProduct = await saveEditedEntityRecord< Product >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
// Redirect using the default anchor behaviour. This way, the usage
|
||||
// of `window.open` is avoided which comes with some edge cases.
|
||||
anchorRef.current?.click();
|
||||
|
||||
if ( onSaveSuccess ) {
|
||||
onSaveSuccess( publishedProduct );
|
||||
}
|
||||
} catch ( error ) {
|
||||
if ( onSaveError ) {
|
||||
onSaveError( error as Error );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'aria-label': __( 'Preview in new tab', 'woocommerce' ),
|
||||
children: __( 'Preview', 'woocommerce' ),
|
||||
target: '_blank',
|
||||
...props,
|
||||
ref( element: HTMLAnchorElement ) {
|
||||
if ( typeof props.ref === 'function' ) props.ref( element );
|
||||
anchorRef.current = element;
|
||||
},
|
||||
'aria-disabled': ariaDisabled,
|
||||
// Note that the href is always passed for a11y support. So
|
||||
// the final rendered element is always an anchor.
|
||||
href: previewLink?.toString(),
|
||||
variant: 'tertiary',
|
||||
onClick: handleClick,
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './use-publish';
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product, ProductStatus } from '@woocommerce/data';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
export function usePublish( {
|
||||
disabled,
|
||||
onClick,
|
||||
onPublishSuccess,
|
||||
onPublishError,
|
||||
...props
|
||||
}: Omit< Button.ButtonProps, 'aria-disabled' | 'variant' | 'children' > & {
|
||||
onPublishSuccess?( product: Product ): void;
|
||||
onPublishError?( error: Error ): void;
|
||||
} ): Button.ButtonProps {
|
||||
const [ productId ] = useEntityProp< number >(
|
||||
'postType',
|
||||
'product',
|
||||
'id'
|
||||
);
|
||||
const [ productStatus ] = useEntityProp< ProductStatus | 'auto-draft' >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
|
||||
const { hasEdits, isDisabled, isBusy } = useSelect(
|
||||
( select ) => {
|
||||
const { hasEditsForEntityRecord, isSavingEntityRecord } =
|
||||
select( 'core' );
|
||||
const { isPostSavingLocked } = select( 'core/editor' );
|
||||
const isSavingLocked = isPostSavingLocked();
|
||||
const isSaving = isSavingEntityRecord< boolean >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
return {
|
||||
isDisabled: isSavingLocked || isSaving,
|
||||
isBusy: isSaving,
|
||||
hasEdits: hasEditsForEntityRecord< boolean >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const isCreating = productStatus === 'auto-draft';
|
||||
const ariaDisabled =
|
||||
disabled || isDisabled || ( productStatus === 'publish' && ! hasEdits );
|
||||
|
||||
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
|
||||
|
||||
async function handleClick( event: MouseEvent< HTMLButtonElement > ) {
|
||||
if ( ariaDisabled ) {
|
||||
return event.preventDefault();
|
||||
}
|
||||
|
||||
if ( onClick ) {
|
||||
onClick( event );
|
||||
}
|
||||
|
||||
try {
|
||||
// The publish button click not only change the status of the product
|
||||
// but also save all the pending changes. So even if the status is
|
||||
// publish it's possible to save the product too.
|
||||
if ( productStatus !== 'publish' ) {
|
||||
await editEntityRecord( 'postType', 'product', productId, {
|
||||
status: 'publish',
|
||||
} );
|
||||
}
|
||||
|
||||
const publishedProduct = await saveEditedEntityRecord< Product >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
if ( onPublishSuccess ) {
|
||||
onPublishSuccess( publishedProduct );
|
||||
}
|
||||
} catch ( error ) {
|
||||
if ( onPublishError ) {
|
||||
onPublishError( error as Error );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
children: isCreating
|
||||
? __( 'Add', 'woocommerce' )
|
||||
: __( 'Save', 'woocommerce' ),
|
||||
...props,
|
||||
'aria-disabled': ariaDisabled,
|
||||
isBusy,
|
||||
variant: 'primary',
|
||||
onClick: handleClick,
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './use-save-draft';
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product, ProductStatus } from '@woocommerce/data';
|
||||
import { Button, Icon } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { check } from '@wordpress/icons';
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
|
||||
export function useSaveDraft( {
|
||||
disabled,
|
||||
onClick,
|
||||
onSaveSuccess,
|
||||
onSaveError,
|
||||
...props
|
||||
}: Omit< Button.ButtonProps, 'aria-disabled' | 'variant' | 'children' > & {
|
||||
onSaveSuccess?( product: Product ): void;
|
||||
onSaveError?( error: Error ): void;
|
||||
} ): Button.ButtonProps {
|
||||
const [ productId ] = useEntityProp< number >(
|
||||
'postType',
|
||||
'product',
|
||||
'id'
|
||||
);
|
||||
const [ productStatus ] = useEntityProp< ProductStatus | 'auto-draft' >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
|
||||
const { hasEdits, isDisabled } = useSelect(
|
||||
( select ) => {
|
||||
const { hasEditsForEntityRecord, isSavingEntityRecord } =
|
||||
select( 'core' );
|
||||
const { isPostSavingLocked } = select( 'core/editor' );
|
||||
const isSavingLocked = isPostSavingLocked();
|
||||
const isSaving = isSavingEntityRecord< boolean >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
return {
|
||||
isDisabled: isSavingLocked || isSaving,
|
||||
hasEdits: hasEditsForEntityRecord< boolean >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const ariaDisabled =
|
||||
disabled || isDisabled || ( productStatus !== 'publish' && ! hasEdits );
|
||||
|
||||
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
|
||||
|
||||
async function handleClick( event: MouseEvent< HTMLButtonElement > ) {
|
||||
if ( ariaDisabled ) {
|
||||
return event.preventDefault();
|
||||
}
|
||||
|
||||
if ( onClick ) {
|
||||
onClick( event );
|
||||
}
|
||||
|
||||
try {
|
||||
await editEntityRecord( 'postType', 'product', productId, {
|
||||
status: 'draft',
|
||||
} );
|
||||
const publishedProduct = await saveEditedEntityRecord< Product >(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
);
|
||||
|
||||
if ( onSaveSuccess ) {
|
||||
onSaveSuccess( publishedProduct );
|
||||
}
|
||||
} catch ( error ) {
|
||||
if ( onSaveError ) {
|
||||
onSaveError( error as Error );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let children: ReactNode;
|
||||
if ( productStatus === 'publish' ) {
|
||||
children = __( 'Switch to draft', 'woocommerce' );
|
||||
} else if ( hasEdits ) {
|
||||
children = __( 'Save draft', 'woocommerce' );
|
||||
} else {
|
||||
children = (
|
||||
<>
|
||||
<Icon icon={ check } />
|
||||
{ __( 'Saved', 'woocommerce' ) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
children,
|
||||
...props,
|
||||
'aria-disabled': ariaDisabled,
|
||||
variant: 'tertiary',
|
||||
onClick: handleClick,
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './preview-button';
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product, ProductStatus } from '@woocommerce/data';
|
||||
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { usePreview } from '../hooks/use-preview';
|
||||
|
||||
export function PreviewButton( {
|
||||
...props
|
||||
}: Omit<
|
||||
Button.AnchorProps,
|
||||
'aria-disabled' | 'variant' | 'href' | 'children'
|
||||
> ) {
|
||||
const [ productStatus ] = useEntityProp< ProductStatus | 'auto-draft' >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
|
||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const previewButtonProps = usePreview( {
|
||||
...props,
|
||||
onSaveSuccess( savedProduct: Product ) {
|
||||
if ( productStatus === 'auto-draft' ) {
|
||||
const url = getNewPath( {}, `/product/${ savedProduct.id }` );
|
||||
navigateTo( { url } );
|
||||
}
|
||||
},
|
||||
onSaveError() {
|
||||
createErrorNotice(
|
||||
__( 'Failed to preview product.', 'woocommerce' )
|
||||
);
|
||||
},
|
||||
} );
|
||||
|
||||
return <Button { ...previewButtonProps } />;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './publish-button';
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product, ProductStatus } from '@woocommerce/data';
|
||||
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { usePublish } from '../hooks/use-publish';
|
||||
|
||||
export function PublishButton(
|
||||
props: Omit< Button.ButtonProps, 'aria-disabled' | 'variant' | 'children' >
|
||||
) {
|
||||
const [ productStatus ] = useEntityProp< ProductStatus | 'auto-draft' >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
|
||||
const isCreating = productStatus === 'auto-draft';
|
||||
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( 'core/notices' );
|
||||
|
||||
const publishButtonProps = usePublish( {
|
||||
...props,
|
||||
onPublishSuccess( savedProduct: Product ) {
|
||||
const noticeContent = isCreating
|
||||
? __( 'Product successfully created.', 'woocommerce' )
|
||||
: __( 'Product published.', 'woocommerce' );
|
||||
const noticeOptions = {
|
||||
icon: '🎉',
|
||||
actions: [
|
||||
{
|
||||
label: __( 'View in store', 'woocommerce' ),
|
||||
// Leave the url to support a11y.
|
||||
url: savedProduct.permalink,
|
||||
onClick( event: MouseEvent< HTMLAnchorElement > ) {
|
||||
event.preventDefault();
|
||||
// Notice actions do not support target anchor prop,
|
||||
// so this forces the page to be opened in a new tab.
|
||||
window.open( savedProduct.permalink, '_blank' );
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
createSuccessNotice( noticeContent, noticeOptions );
|
||||
|
||||
if ( productStatus === 'auto-draft' ) {
|
||||
const url = getNewPath( {}, `/product/${ savedProduct.id }` );
|
||||
navigateTo( { url } );
|
||||
}
|
||||
},
|
||||
onPublishError() {
|
||||
const noticeContent = isCreating
|
||||
? __( 'Failed to create product.', 'woocommerce' )
|
||||
: __( 'Failed to publish product.', 'woocommerce' );
|
||||
|
||||
createErrorNotice( noticeContent );
|
||||
},
|
||||
} );
|
||||
|
||||
return <Button { ...publishButtonProps } />;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './save-draft-button';
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Product, ProductStatus } from '@woocommerce/data';
|
||||
import { getNewPath, navigateTo } from '@woocommerce/navigation';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useSaveDraft } from '../hooks/use-save-draft';
|
||||
|
||||
export function SaveDraftButton(
|
||||
props: Omit< Button.ButtonProps, 'aria-disabled' | 'variant' | 'children' >
|
||||
) {
|
||||
const [ productStatus ] = useEntityProp< ProductStatus | 'auto-draft' >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
|
||||
const { createSuccessNotice, createErrorNotice } =
|
||||
useDispatch( 'core/notices' );
|
||||
|
||||
const saveDraftButtonProps = useSaveDraft( {
|
||||
...props,
|
||||
onSaveSuccess( savedProduct: Product ) {
|
||||
createSuccessNotice(
|
||||
__( 'Product saved as draft.', 'woocommerce' )
|
||||
);
|
||||
|
||||
if ( productStatus === 'auto-draft' ) {
|
||||
const url = getNewPath( {}, `/product/${ savedProduct.id }` );
|
||||
navigateTo( { url } );
|
||||
}
|
||||
},
|
||||
onSaveError() {
|
||||
createErrorNotice(
|
||||
__( 'Failed to update product.', 'woocommerce' )
|
||||
);
|
||||
},
|
||||
} );
|
||||
|
||||
return <Button { ...saveDraftButtonProps } />;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-radio",
|
||||
"title": "Product radio control",
|
||||
"category": "woocommerce",
|
||||
"description": "The product radio.",
|
||||
"keywords": [ "products", "radio", "input" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
},
|
||||
"default": [],
|
||||
"__experimentalRole": "content"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": true,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false,
|
||||
"__experimentalToolbar": false
|
||||
},
|
||||
"editorStyle": "file:./editor.css"
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import type { BlockAttributes } from '@wordpress/blocks';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { sanitizeHTML } from '../../utils/sanitize-html';
|
||||
|
||||
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { description, options, property, title } = attributes;
|
||||
const [ value, setValue ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
property
|
||||
);
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<RadioControl
|
||||
label={
|
||||
<>
|
||||
<span className="wp-block-woocommerce-product-radio__title">
|
||||
{ title }
|
||||
</span>
|
||||
<span
|
||||
className="wp-block-woocommerce-product-radio__description"
|
||||
dangerouslySetInnerHTML={ sanitizeHTML(
|
||||
description
|
||||
) }
|
||||
/>
|
||||
</>
|
||||
}
|
||||
selected={ value }
|
||||
options={ options }
|
||||
onChange={ ( selected ) => setValue( selected || '' ) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
.wp-block-woocommerce-product-radio {
|
||||
.components-base-control__label {
|
||||
text-transform: none;
|
||||
font-weight: 400;
|
||||
> span {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
margin-bottom: $gap-large;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e1e1e;
|
||||
margin-bottom: $gap-smaller;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 13px;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
||||
.components-base-control__field > .components-v-stack {
|
||||
gap: $gap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import initBlock from '../../utils/init-block';
|
||||
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, settings } );
|
|
@ -6,6 +6,7 @@
|
|||
@import 'components/header/style.scss';
|
||||
@import 'components/images/editor.scss';
|
||||
@import 'components/block-editor/style.scss';
|
||||
@import 'components/radio/editor.scss';
|
||||
@import 'components/section/editor.scss';
|
||||
@import 'components/tab/editor.scss';
|
||||
@import 'components/tabs/style.scss';
|
||||
|
|
|
@ -167,7 +167,7 @@ export const getPages = () => {
|
|||
} );
|
||||
}
|
||||
|
||||
if ( window.wcAdminFeatures[ 'block-editor-feature-enabled' ] ) {
|
||||
if ( window.wcAdminFeatures[ 'product-block-editor' ] ) {
|
||||
pages.push( {
|
||||
container: ProductPage,
|
||||
path: '/add-product',
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
body.woocommerce-page {
|
||||
.components-button.is-primary {
|
||||
&:not(:disabled) {
|
||||
&:not(:disabled):not([aria-disabled='true']):hover {
|
||||
color: $studio-white;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ declare global {
|
|||
wcAdminFeatures: {
|
||||
'activity-panels': boolean;
|
||||
analytics: boolean;
|
||||
'block-editor-feature-enabled': boolean;
|
||||
'product-block-editor': boolean;
|
||||
coupons: boolean;
|
||||
'customer-effort-score-tracks': boolean;
|
||||
homescreen: boolean;
|
||||
|
|
|
@ -504,7 +504,14 @@ export const initProductScreenTracks = () => {
|
|||
|
||||
document
|
||||
.querySelector( '.save_attributes' )
|
||||
?.addEventListener( 'click', () => {
|
||||
?.addEventListener( 'click', ( event ) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.classList.contains( 'disabled' )
|
||||
) {
|
||||
// skip in case the button is disabled
|
||||
return;
|
||||
}
|
||||
const newAttributesCount = document.querySelectorAll(
|
||||
'.woocommerce_attribute'
|
||||
).length;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Change variations dropdown menu visibility
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix global button aria-disabled style
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add tax class to product editor template
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding charge sales tax field to product block editor template.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
|
||||
Default to sorting orders by date (desc) when HPOS is active.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
update select all to checkbox in menu editor
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix table alias issue in order field queries.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Add sort order to migration script for consistency.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Register product editor blocks server-side
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Show tooltip in Save attributes button instead of using title attribute
|
|
@ -2,7 +2,7 @@
|
|||
"features": {
|
||||
"activity-panels": true,
|
||||
"analytics": true,
|
||||
"block-editor-feature-enabled": false,
|
||||
"product-block-editor": false,
|
||||
"coupons": true,
|
||||
"customer-effort-score-tracks": true,
|
||||
"import-products-task": true,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"features": {
|
||||
"activity-panels": true,
|
||||
"analytics": true,
|
||||
"block-editor-feature-enabled": true,
|
||||
"product-block-editor": true,
|
||||
"coupons": true,
|
||||
"customer-effort-score-tracks": true,
|
||||
"import-products-task": true,
|
||||
|
|
|
@ -1018,8 +1018,13 @@ $default-line-height: 18px;
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.variation_actions {
|
||||
max-width: 131px;
|
||||
}
|
||||
|
||||
.toolbar-top {
|
||||
.button {
|
||||
.button,
|
||||
.select {
|
||||
margin: 1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,11 @@ jQuery( function ( $ ) {
|
|||
);
|
||||
},
|
||||
|
||||
create_variations: function () {
|
||||
create_variations: function ( event ) {
|
||||
if ( $( this ).hasClass( 'disabled' ) ) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
var new_attribute_data = $(
|
||||
'.woocommerce_variation_new_attribute_data'
|
||||
);
|
||||
|
@ -655,8 +659,8 @@ jQuery( function ( $ ) {
|
|||
} );
|
||||
|
||||
$( '.wc-metaboxes-wrapper' ).on(
|
||||
'click',
|
||||
'a.do_variation_action',
|
||||
'change',
|
||||
'#field_to_edit',
|
||||
this.do_variation_action
|
||||
);
|
||||
|
||||
|
@ -1195,7 +1199,7 @@ jQuery( function ( $ ) {
|
|||
* Actions
|
||||
*/
|
||||
do_variation_action: function () {
|
||||
var do_variation_action = $( 'select.variation_actions' ).val(),
|
||||
var do_variation_action = $( this ).val(),
|
||||
data = {},
|
||||
changes = 0,
|
||||
value;
|
||||
|
@ -1344,11 +1348,9 @@ jQuery( function ( $ ) {
|
|||
if ( parseInt( wrapper.attr( 'data-total' ) ) > 0 ) {
|
||||
$( '.add-variation-container' ).addClass( 'hidden' );
|
||||
$( '#field_to_edit' ).removeClass( 'hidden' );
|
||||
$( 'a.do_variation_action' ).removeClass( 'hidden' );
|
||||
} else {
|
||||
$( '.add-variation-container' ).removeClass( 'hidden' );
|
||||
$( '#field_to_edit' ).addClass( 'hidden' );
|
||||
$( 'a.do_variation_action' ).addClass( 'hidden' );
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1451,7 +1453,7 @@ jQuery( function ( $ ) {
|
|||
|
||||
if ( page_nav.is( ':hidden' ) ) {
|
||||
$( 'option, optgroup', '.variation_actions' ).show();
|
||||
$( '.variation_actions' ).val( 'delete_all' );
|
||||
$( '.variation_actions' ).val( 'bulk_actions' );
|
||||
$( '#variable_product_options' ).find( '.toolbar' ).show();
|
||||
page_nav.show();
|
||||
$( '.pagination-links', page_nav ).hide();
|
||||
|
@ -1498,13 +1500,13 @@ jQuery( function ( $ ) {
|
|||
toolbar.not( '.toolbar-top, .toolbar-buttons' ).hide();
|
||||
page_nav.hide();
|
||||
$( 'option, optgroup', variation_action ).hide();
|
||||
$( '.variation_actions' ).val( 'delete_all' );
|
||||
$( '.variation_actions' ).val( 'bulk_actions' );
|
||||
$( 'option[data-global="true"]', variation_action ).show();
|
||||
} else {
|
||||
toolbar.show();
|
||||
page_nav.show();
|
||||
$( 'option, optgroup', variation_action ).show();
|
||||
$( '.variation_actions' ).val( 'delete_all' );
|
||||
$( '.variation_actions' ).val( 'bulk_actions' );
|
||||
|
||||
// Show/hide links
|
||||
if ( 1 === total_pages ) {
|
||||
|
|
|
@ -750,7 +750,11 @@ jQuery( function ( $ ) {
|
|||
);
|
||||
|
||||
// Save attributes and update variations.
|
||||
$( '.save_attributes' ).on( 'click', function () {
|
||||
$( '.save_attributes' ).on( 'click', function ( event ) {
|
||||
if ( $( this ).hasClass( 'disabled' ) ) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
$( '.product_attributes' ).block( {
|
||||
message: null,
|
||||
overlayCSS: {
|
||||
|
|
|
@ -50,17 +50,14 @@ jQuery( function ( $ ) {
|
|||
attributes_and_variations_data
|
||||
)
|
||||
) {
|
||||
if ( ! $save_button.is( ':disabled' ) ) {
|
||||
$save_button.attr( 'disabled', 'disabled' );
|
||||
$save_button.attr(
|
||||
'title',
|
||||
woocommerce_admin_meta_boxes.i18n_save_attribute_variation_tip
|
||||
);
|
||||
if ( ! $save_button.hasClass( 'disabled' ) ) {
|
||||
$save_button.addClass( 'disabled' );
|
||||
$save_button.attr( 'aria-disabled', true );
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
$save_button.removeClass( 'disabled' );
|
||||
$save_button.removeAttr( 'aria-disabled' );
|
||||
}
|
||||
$save_button.removeAttr( 'disabled' );
|
||||
$save_button.removeAttr( 'title' );
|
||||
};
|
||||
|
||||
// Run tipTip
|
||||
|
@ -79,6 +76,30 @@ jQuery( function ( $ ) {
|
|||
|
||||
runTipTip();
|
||||
|
||||
$( '.save_attributes' ).tipTip( {
|
||||
content: function () {
|
||||
return $( '.save_attributes' ).hasClass( 'disabled' )
|
||||
? woocommerce_admin_meta_boxes.i18n_save_attribute_variation_tip
|
||||
: '';
|
||||
},
|
||||
fadeIn: 50,
|
||||
fadeOut: 50,
|
||||
delay: 200,
|
||||
keepAlive: true,
|
||||
} );
|
||||
|
||||
$( '.create-variations' ).tipTip( {
|
||||
content: function () {
|
||||
return $( '.create-variations' ).hasClass( 'disabled' )
|
||||
? woocommerce_admin_meta_boxes.i18n_save_attribute_variation_tip
|
||||
: '';
|
||||
},
|
||||
fadeIn: 50,
|
||||
fadeOut: 50,
|
||||
delay: 200,
|
||||
keepAlive: true,
|
||||
} );
|
||||
|
||||
$( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox > h3', function () {
|
||||
var metabox = $( this ).parent( '.wc-metabox' );
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
* www.drewwilson.com
|
||||
* code.drewwilson.com/entry/tiptip-jquery-plugin
|
||||
*
|
||||
* Version 1.3 - Updated: Mar. 23, 2010
|
||||
* Version 1.3.1 - Updated: Mar. 30, 2023
|
||||
*
|
||||
* This is a custom version of TipTip. This file has been locally modified for specific requirements.
|
||||
* Since the original version is no longer maintained, the changes were not submitted back to the original author.
|
||||
*
|
||||
* This Plug-In will create a custom tooltip to replace the default
|
||||
* browser tooltip. It is extremely lightweight and very smart in
|
||||
|
@ -31,7 +34,7 @@
|
|||
fadeIn: 200,
|
||||
fadeOut: 200,
|
||||
attribute: "title",
|
||||
content: false, // HTML or String to fill TipTIp with
|
||||
content: false, // HTML or String or callback to fill TipTIp with
|
||||
enter: function(){},
|
||||
exit: function(){}
|
||||
};
|
||||
|
@ -98,8 +101,12 @@
|
|||
}
|
||||
|
||||
function active_tiptip(){
|
||||
var content = typeof opts.content === 'function' ? opts.content() : org_title;
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
opts.enter.call(this);
|
||||
tiptip_content.html(org_title);
|
||||
tiptip_content.html(content);
|
||||
tiptip_holder.hide().css("margin","0");
|
||||
tiptip_holder.removeAttr('class');
|
||||
tiptip_arrow.removeAttr("style");
|
||||
|
|
|
@ -218,7 +218,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
|
|||
|
||||
if ( OrderUtil::orders_cache_usage_is_enabled() ) {
|
||||
$order_cache = wc_get_container()->get( OrderCache::class );
|
||||
$order_cache->update_if_cached( $this );
|
||||
$order_cache->remove( $this->get_id() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -225,7 +225,7 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
|
|||
'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ),
|
||||
),
|
||||
'urls' => array(
|
||||
'add_product' => Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'block-editor-feature-enabled' ) ? esc_url_raw( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) ) : null,
|
||||
'add_product' => Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'product-block-editor' ) ? esc_url_raw( admin_url( 'admin.php?page=wc-admin&path=/add-product' ) ) : null,
|
||||
'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null,
|
||||
'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null,
|
||||
),
|
||||
|
|
|
@ -375,9 +375,12 @@ class WC_Admin_Menus {
|
|||
?>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="button-controls">
|
||||
<p class="button-controls" data-items-type="posttype-woocommerce-endpoints">
|
||||
<span class="list-controls">
|
||||
<a href="<?php echo esc_url( admin_url( 'nav-menus.php?page-tab=all&selectall=1#posttype-woocommerce-endpoints' ) ); ?>" class="select-all"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></a>
|
||||
<label>
|
||||
<input type="checkbox" class="select-all" />
|
||||
<?php esc_html_e( 'Select all', 'woocommerce' ); ?>
|
||||
</label>
|
||||
</span>
|
||||
<span class="add-to-menu">
|
||||
<button type="submit" class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to menu', 'woocommerce' ); ?>" name="add-post-type-menu-item" id="submit-posttype-woocommerce-endpoints"><?php esc_html_e( 'Add to menu', 'woocommerce' ); ?></button>
|
||||
|
@ -424,7 +427,7 @@ class WC_Admin_Menus {
|
|||
* Maybe add new management product experience.
|
||||
*/
|
||||
public function maybe_add_new_product_management_experience() {
|
||||
if ( Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'block-editor-feature-enabled' ) ) {
|
||||
if ( Features::is_enabled( 'new-product-management-experience' ) || Features::is_enabled( 'product-block-editor' ) ) {
|
||||
global $submenu;
|
||||
if ( isset( $submenu['edit.php?post_type=product'][10] ) ) {
|
||||
// Disable phpcs since we need to override submenu classes.
|
||||
|
|
|
@ -108,7 +108,7 @@ $icon_url = WC_ADMIN_IMAGES_FOLDER_URL . '/icons/global-a
|
|||
<span class="expand-close">
|
||||
<a href="#" class="expand_all"><?php esc_html_e( 'Expand', 'woocommerce' ); ?></a> / <a href="#" class="close_all"><?php esc_html_e( 'Close', 'woocommerce' ); ?></a>
|
||||
</span>
|
||||
<button type="button" class="button save_attributes button-primary" disabled="disabled" title="<?php echo esc_html_e( 'Make sure you enter the name and values for each attribute.', 'woocommerce' ); ?>"><?php esc_html_e( 'Save attributes', 'woocommerce' ); ?></button>
|
||||
<button type="button" aria-disabled="true" class="button save_attributes button-primary disabled"><?php esc_html_e( 'Save attributes', 'woocommerce' ); ?></button>
|
||||
</div>
|
||||
<?php do_action( 'woocommerce_product_options_attributes' ); ?>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,7 @@ $arrow_img_url = WC_ADMIN_IMAGES_FOLDER_URL . '/product_data/no-variati
|
|||
require __DIR__ . '/html-product-attribute-inner.php';
|
||||
?>
|
||||
<div class="toolbar">
|
||||
<button type="button" class="button button-primary create-variations" disabled="disabled" title="<?php echo esc_html_e( 'Make sure you enter the name and values for each attribute.', 'woocommerce' ); ?>"><?php esc_html_e( 'Create variations', 'woocommerce' ); ?></button>
|
||||
<button type="button" aria-disabled="true" class="button button-primary create-variations disabled"><?php esc_html_e( 'Create variations', 'woocommerce' ); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -97,7 +97,8 @@ $arrow_img_url = WC_ADMIN_IMAGES_FOLDER_URL . '/product_data/no-variati
|
|||
<div class="toolbar toolbar-top">
|
||||
<button type="button" class="button generate_variations"><?php esc_html_e( 'Generate variations', 'woocommerce' ); ?></button>
|
||||
<button type="button" class="button add_variation_manually"><?php esc_html_e( 'Add manually', 'woocommerce' ); ?></button>
|
||||
<select id="field_to_edit" class="variation_actions hidden">
|
||||
<select id="field_to_edit" class="select variation_actions hidden">
|
||||
<option value="bulk_actions" disabled>Bulk actions</option>
|
||||
<option value="delete_all"><?php esc_html_e( 'Delete all variations', 'woocommerce' ); ?></option>
|
||||
<optgroup label="<?php esc_attr_e( 'Status', 'woocommerce' ); ?>">
|
||||
<option value="toggle_enabled"><?php esc_html_e( 'Toggle "Enabled"', 'woocommerce' ); ?></option>
|
||||
|
@ -135,7 +136,6 @@ $arrow_img_url = WC_ADMIN_IMAGES_FOLDER_URL . '/product_data/no-variati
|
|||
<?php do_action( 'woocommerce_variable_product_bulk_edit_actions' ); ?>
|
||||
<?php /* phpcs:enable */ ?>
|
||||
</select>
|
||||
<a class="button bulk_edit do_variation_action hidden"><?php esc_html_e( 'Go', 'woocommerce' ); ?></a>
|
||||
|
||||
<div class="variations-pagenav">
|
||||
<?php /* translators: variations count */ ?>
|
||||
|
|
|
@ -513,6 +513,64 @@ class WC_Post_Types {
|
|||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/product-radio',
|
||||
array(
|
||||
'title' => __( 'Charge sales tax on', 'woocommerce' ),
|
||||
'property' => 'tax_status',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __( 'Product and shipping', 'woocommerce' ),
|
||||
'value' => 'taxable',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Only shipping', 'woocommerce' ),
|
||||
'value' => 'shipping',
|
||||
),
|
||||
array(
|
||||
'label' => __( "Don't charge tax", 'woocommerce' ),
|
||||
'value' => 'none',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'woocommerce/collapsible',
|
||||
array(
|
||||
'toggleText' => __( 'Advanced', 'woocommerce' ),
|
||||
'initialCollapsed' => true,
|
||||
'persistRender' => true,
|
||||
),
|
||||
array(
|
||||
array(
|
||||
'woocommerce/product-radio',
|
||||
array(
|
||||
'title' => __( 'Tax class', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: Learn more link opening tag. %2$s: Learn more link closing tag.*/
|
||||
__( 'Apply a tax rate if this product qualifies for tax reduction or exemption. %1$sLearn more%2$s.', 'woocommerce' ),
|
||||
'<a href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class" target="_blank" rel="noreferrer">',
|
||||
'</a>'
|
||||
),
|
||||
'property' => 'tax_class',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => __( 'Standard', 'woocommerce' ),
|
||||
'value' => '',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Reduced rate', 'woocommerce' ),
|
||||
'value' => 'reduced-rate',
|
||||
),
|
||||
array(
|
||||
'label' => __( 'Zero rate', 'woocommerce' ),
|
||||
'value' => 'zero-rate',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -40,7 +40,7 @@ class Features {
|
|||
protected static $beta_features = array(
|
||||
'navigation',
|
||||
'new-product-management-experience',
|
||||
'block-editor-feature-enabled',
|
||||
'product-block-editor',
|
||||
'settings',
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce Product Editor Block Registration
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
|
||||
/**
|
||||
* Product block registration and style registration functionality.
|
||||
*/
|
||||
class BlockRegistry {
|
||||
/**
|
||||
* The directory where blocks are stored after build.
|
||||
*/
|
||||
const BLOCKS_DIR = 'product-editor/blocks';
|
||||
|
||||
/**
|
||||
* Array of all available product blocks.
|
||||
*/
|
||||
const PRODUCT_BLOCKS = [
|
||||
'woocommerce/product-name',
|
||||
'woocommerce/product-pricing',
|
||||
'woocommerce/product-section',
|
||||
'woocommerce/product-tab',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a file path for a given block file.
|
||||
*
|
||||
* @param string $path File path.
|
||||
*/
|
||||
public function get_file_path( $path ) {
|
||||
return WC_ABSPATH . WCAdminAssets::get_path( 'js' ) . trailingslashit( self::BLOCKS_DIR ) . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all the product blocks.
|
||||
*/
|
||||
public function register_product_blocks() {
|
||||
foreach ( self::PRODUCT_BLOCKS as $block_name ) {
|
||||
$this->register_block( $block_name );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the block name without the "woocommerce/" prefix.
|
||||
*
|
||||
* @param string $block_name Block name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function remove_block_prefix( $block_name ) {
|
||||
if ( 0 === strpos( $block_name, 'woocommerce/' ) ) {
|
||||
return substr_replace( $block_name, '', 0, strlen( 'woocommerce/' ) );
|
||||
}
|
||||
|
||||
return $block_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single block.
|
||||
*
|
||||
* @param string $block_name Block name.
|
||||
*
|
||||
* @return WP_Block_Type|false The registered block type on success, or false on failure.
|
||||
*/
|
||||
public function register_block( $block_name ) {
|
||||
$block_name = $this->remove_block_prefix( $block_name );
|
||||
$block_json_file = $this->get_file_path( $block_name . '/block.json' );
|
||||
|
||||
if ( ! file_exists( $block_json_file ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$metadata = json_decode( file_get_contents( $block_json_file ), true );
|
||||
if ( ! is_array( $metadata ) || ! $metadata['name'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$registry = \WP_Block_Type_Registry::get_instance();
|
||||
|
||||
if ( $registry->is_registered( $metadata['name'] ) ) {
|
||||
$registry->unregister( $metadata['name'] );
|
||||
}
|
||||
|
||||
return register_block_type_from_metadata( $block_json_file );
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +1,22 @@
|
|||
<?php
|
||||
/**
|
||||
* WooCommerce Block Editor Feature Endabled
|
||||
* WooCommerce Product Block Editor
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features;
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\Features;
|
||||
use Automattic\WooCommerce\Admin\Features\TransientNotices;
|
||||
use Automattic\WooCommerce\Admin\PageController;
|
||||
use Automattic\WooCommerce\Internal\Admin\Loader;
|
||||
use WP_Block_Editor_Context;
|
||||
|
||||
/**
|
||||
* Loads assets related to the new product management experience page.
|
||||
* Loads assets related to the product block editor.
|
||||
*/
|
||||
class BlockEditorFeatureEnabled {
|
||||
class Init {
|
||||
|
||||
const FEATURE_ID = 'block-editor-feature-enabled';
|
||||
const FEATURE_ID = 'product-block-editor';
|
||||
|
||||
/**
|
||||
* Option name used to toggle this feature.
|
||||
|
@ -33,6 +34,8 @@ class BlockEditorFeatureEnabled {
|
|||
if ( Features::is_enabled( self::FEATURE_ID ) ) {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_rest_base_config' ) );
|
||||
$block_registry = new BlockRegistry();
|
||||
$block_registry->register_product_blocks();
|
||||
}
|
||||
}
|
||||
|
|
@ -365,7 +365,7 @@ class ListTable extends WP_List_Table {
|
|||
$direction = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ?? '' ) ) );
|
||||
|
||||
if ( ! in_array( $field, $sortable, true ) ) {
|
||||
$this->order_query_args['orderby'] = 'id';
|
||||
$this->order_query_args['orderby'] = 'date';
|
||||
$this->order_query_args['order'] = 'DESC';
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -294,7 +294,8 @@ LEFT JOIN $orders_table orders ON posts.ID = orders.id
|
|||
WHERE
|
||||
posts.post_type IN ($order_post_type_placeholders)
|
||||
AND posts.post_status != 'auto-draft'
|
||||
AND orders.id IS NULL",
|
||||
AND orders.id IS NULL
|
||||
ORDER BY posts.ID ASC",
|
||||
$order_post_types
|
||||
);
|
||||
// phpcs:enable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
|
||||
|
@ -305,6 +306,7 @@ SELECT posts.ID FROM $wpdb->posts posts
|
|||
INNER JOIN $orders_table orders ON posts.id=orders.id
|
||||
WHERE posts.post_type = '" . self::PLACEHOLDER_ORDER_POST_TYPE . "'
|
||||
AND orders.status not in ( 'auto-draft' )
|
||||
ORDER BY posts.id ASC
|
||||
";
|
||||
break;
|
||||
case self::ID_TYPE_DIFFERENT_UPDATE_DATE:
|
||||
|
@ -317,6 +319,7 @@ JOIN $wpdb->posts posts on posts.ID = orders.id
|
|||
WHERE
|
||||
posts.post_type IN ($order_post_type_placeholders)
|
||||
AND orders.date_updated_gmt $operator posts.post_modified_gmt
|
||||
ORDER BY orders.id ASC
|
||||
",
|
||||
$order_post_types
|
||||
);
|
||||
|
|
|
@ -246,7 +246,7 @@ class OrdersTableFieldQuery {
|
|||
$this->table_aliases[] = $alias;
|
||||
|
||||
if ( $join ) {
|
||||
$this->join[] = $join;
|
||||
$this->join[ $alias ] = $join;
|
||||
}
|
||||
|
||||
return $alias;
|
||||
|
|
|
@ -755,9 +755,9 @@ class OrdersTableQuery {
|
|||
|
||||
if ( empty( $on ) ) {
|
||||
if ( $this->tables['orders'] === $table ) {
|
||||
$on = "{$this->tables['orders']}.id = {$alias}.id";
|
||||
$on = "`{$this->tables['orders']}`.id = `{$alias}`.id";
|
||||
} else {
|
||||
$on = "{$this->tables['orders']}.id = {$alias}.order_id";
|
||||
$on = "`{$this->tables['orders']}`.id = `{$alias}`.order_id";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -775,8 +775,8 @@ class OrdersTableQuery {
|
|||
}
|
||||
|
||||
$sql_join = '';
|
||||
$sql_join .= "{$join_type} JOIN {$table} ";
|
||||
$sql_join .= ( $alias !== $table ) ? "AS {$alias} " : '';
|
||||
$sql_join .= "{$join_type} JOIN `{$table}` ";
|
||||
$sql_join .= ( $alias !== $table ) ? "AS `{$alias}` " : '';
|
||||
$sql_join .= "ON ( {$on} )";
|
||||
|
||||
$this->join[ $alias ] = $sql_join;
|
||||
|
|
|
@ -179,7 +179,6 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
await page.selectOption( '#field_to_edit', 'toggle_downloadable', {
|
||||
force: true,
|
||||
} );
|
||||
await page.click( 'a.do_variation_action' );
|
||||
await page.click(
|
||||
'#variable_product_options .toolbar-top a.expand_all'
|
||||
);
|
||||
|
@ -196,7 +195,6 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
await page.click( 'a[href="#variable_product_options"]' );
|
||||
await page.waitForLoadState( 'networkidle' );
|
||||
await page.selectOption( '#field_to_edit', 'delete_all' );
|
||||
await page.click( 'a.do_variation_action' );
|
||||
await page.waitForSelector( '.woocommerce_variation', {
|
||||
state: 'detached',
|
||||
} );
|
||||
|
|
|
@ -269,4 +269,23 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
|
|||
$this->assertEquals( $coupon->get_id(), $coupon_data['id'] );
|
||||
$this->assertEquals( $coupon_code, $coupon_data['code'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testDox Cache does not interfere if wc_get_order returns a different class than WC_Order.
|
||||
*/
|
||||
public function test_cache_does_not_interferes_with_order_object() {
|
||||
add_action(
|
||||
'woocommerce_new_order',
|
||||
function( $order_id ) {
|
||||
// this makes the cache store a specific order class instance, but it's quickly replaced by a generic one
|
||||
// as we're in the middle of a save and this gets executed before the logic in WC_Abstract_Order.
|
||||
$order = wc_get_order( $order_id );
|
||||
}
|
||||
);
|
||||
$order = new WC_Order();
|
||||
$order->save();
|
||||
|
||||
$order = wc_get_order( $order->get_id() );
|
||||
$this->assertInstanceOf( Automattic\WooCommerce\Admin\Overrides\Order::class, $order );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -293,4 +293,22 @@ class DataSynchronizerTests extends WC_Unit_Test_Case {
|
|||
'Meta data deleted from the CPT datastore should also be deleted from the HPOS datastore.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testDox Orders for migration are picked by ID sorted.
|
||||
*/
|
||||
public function test_migration_sort() {
|
||||
global $wpdb;
|
||||
$order1 = wc_get_order( OrderHelper::create_order() );
|
||||
$order2 = wc_get_order( OrderHelper::create_order() );
|
||||
|
||||
// Let's update order1 id to be greater than order2 id.
|
||||
// phpcs:ignore
|
||||
$max_id = $wpdb->get_var( "SELECT MAX(id) FROM $wpdb->posts" );
|
||||
$wpdb->update( $wpdb->posts, array( 'ID' => $max_id + 1 ), array( 'ID' => $order1->get_id() ) );
|
||||
|
||||
$orders_to_migrate = $this->sut->get_next_batch_to_process( 2 );
|
||||
$this->assertEquals( $order2->get_id(), $orders_to_migrate[0] );
|
||||
$this->assertEquals( $max_id + 1, $orders_to_migrate[1] );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue