Merge branch 'trunk' into add/add-changelog-file-tool
This commit is contained in:
commit
90550c3394
|
@ -201,7 +201,9 @@
|
||||||
"@wordpress/interface",
|
"@wordpress/interface",
|
||||||
"@wordpress/router",
|
"@wordpress/router",
|
||||||
"@wordpress/edit-site",
|
"@wordpress/edit-site",
|
||||||
"@wordpress/private-apis"
|
"@wordpress/private-apis",
|
||||||
|
"@wordpress/dataviews",
|
||||||
|
"@wordpress/icons"
|
||||||
],
|
],
|
||||||
"packages": [
|
"packages": [
|
||||||
"@woocommerce/block-templates",
|
"@woocommerce/block-templates",
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add sidebar and dataviews list to the experimental dataviews products page.
|
|
@ -56,6 +56,7 @@
|
||||||
"@wordpress/compose": "wp-6.0",
|
"@wordpress/compose": "wp-6.0",
|
||||||
"@wordpress/core-data": "wp-6.0",
|
"@wordpress/core-data": "wp-6.0",
|
||||||
"@wordpress/data": "wp-6.0",
|
"@wordpress/data": "wp-6.0",
|
||||||
|
"@wordpress/dataviews": "^4.2.0",
|
||||||
"@wordpress/date": "wp-6.0",
|
"@wordpress/date": "wp-6.0",
|
||||||
"@wordpress/deprecated": "wp-6.0",
|
"@wordpress/deprecated": "wp-6.0",
|
||||||
"@wordpress/edit-post": "wp-6.0",
|
"@wordpress/edit-post": "wp-6.0",
|
||||||
|
@ -64,7 +65,7 @@
|
||||||
"@wordpress/hooks": "wp-6.0",
|
"@wordpress/hooks": "wp-6.0",
|
||||||
"@wordpress/html-entities": "wp-6.0",
|
"@wordpress/html-entities": "wp-6.0",
|
||||||
"@wordpress/i18n": "wp-6.0",
|
"@wordpress/i18n": "wp-6.0",
|
||||||
"@wordpress/icons": "wp-6.0",
|
"@wordpress/icons": "10.6.0",
|
||||||
"@wordpress/interface": "wp-6.0",
|
"@wordpress/interface": "wp-6.0",
|
||||||
"@wordpress/keyboard-shortcuts": "wp-6.0",
|
"@wordpress/keyboard-shortcuts": "wp-6.0",
|
||||||
"@wordpress/keycodes": "wp-6.0",
|
"@wordpress/keycodes": "wp-6.0",
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const LAYOUT_GRID = 'grid';
|
||||||
|
export const LAYOUT_TABLE = 'table';
|
||||||
|
export const LAYOUT_LIST = 'list';
|
||||||
|
|
||||||
|
export const OPERATOR_IS = 'is';
|
||||||
|
export const OPERATOR_IS_NOT = 'isNot';
|
||||||
|
export const OPERATOR_IS_ANY = 'isAny';
|
||||||
|
export const OPERATOR_IS_NONE = 'isNone';
|
|
@ -13,12 +13,16 @@ import {
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { unlock } from '../lock-unlock';
|
import { unlock } from '../lock-unlock';
|
||||||
|
import useLayoutAreas from './router';
|
||||||
|
import { Layout } from './layout';
|
||||||
|
|
||||||
const { RouterProvider } = unlock( routerPrivateApis );
|
const { RouterProvider } = unlock( routerPrivateApis );
|
||||||
const { GlobalStylesProvider } = unlock( editorPrivateApis );
|
const { GlobalStylesProvider } = unlock( editorPrivateApis );
|
||||||
|
|
||||||
function ProductsLayout() {
|
function ProductsLayout() {
|
||||||
return <div>Initial Products Layout</div>;
|
// This ensures the edited entity id and type are initialized properly.
|
||||||
|
const route = useLayoutAreas();
|
||||||
|
return <Layout route={ route } />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProductsApp() {
|
export function ProductsApp() {
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement, Fragment, useRef } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
useViewportMatch,
|
||||||
|
useResizeObserver,
|
||||||
|
useReducedMotion,
|
||||||
|
} from '@wordpress/compose';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
EditorSnackbars,
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
privateApis as editorPrivateApis,
|
||||||
|
} from '@wordpress/editor';
|
||||||
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
|
import {
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__unstableMotion as motion,
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__unstableAnimatePresence as AnimatePresence,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import SidebarContent from './sidebar';
|
||||||
|
import SiteHub from './site-hub';
|
||||||
|
import { Route } from './router';
|
||||||
|
import { unlock } from '../lock-unlock';
|
||||||
|
|
||||||
|
const { NavigableRegion } = unlock( editorPrivateApis );
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 0.3;
|
||||||
|
|
||||||
|
type LayoutProps = {
|
||||||
|
route: Route;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Layout( { route }: LayoutProps ) {
|
||||||
|
const [ fullResizer ] = useResizeObserver();
|
||||||
|
const toggleRef = useRef< HTMLAnchorElement >( null );
|
||||||
|
const isMobileViewport = useViewportMatch( 'medium', '<' );
|
||||||
|
const disableMotion = useReducedMotion();
|
||||||
|
|
||||||
|
const { key: routeKey, areas, widths } = route;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ fullResizer }
|
||||||
|
<div className="edit-site-layout">
|
||||||
|
<div className="edit-site-layout__content">
|
||||||
|
{ /*
|
||||||
|
The NavigableRegion must always be rendered and not use
|
||||||
|
`inert` otherwise `useNavigateRegions` will fail.
|
||||||
|
*/ }
|
||||||
|
{ ( ! isMobileViewport || ! areas.mobile ) && (
|
||||||
|
<NavigableRegion
|
||||||
|
ariaLabel={ __( 'Navigation', 'woocommerce' ) }
|
||||||
|
className="edit-site-layout__sidebar-region"
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={ { opacity: 0 } }
|
||||||
|
animate={ { opacity: 1 } }
|
||||||
|
exit={ { opacity: 0 } }
|
||||||
|
transition={ {
|
||||||
|
type: 'tween',
|
||||||
|
duration:
|
||||||
|
// Disable transition in mobile to emulate a full page transition.
|
||||||
|
disableMotion || isMobileViewport
|
||||||
|
? 0
|
||||||
|
: ANIMATION_DURATION,
|
||||||
|
ease: 'easeOut',
|
||||||
|
} }
|
||||||
|
className="edit-site-layout__sidebar"
|
||||||
|
>
|
||||||
|
<SiteHub
|
||||||
|
ref={ toggleRef }
|
||||||
|
isTransparent={ false }
|
||||||
|
/>
|
||||||
|
<SidebarContent routeKey={ routeKey }>
|
||||||
|
{ areas.sidebar }
|
||||||
|
</SidebarContent>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</NavigableRegion>
|
||||||
|
) }
|
||||||
|
|
||||||
|
<EditorSnackbars />
|
||||||
|
|
||||||
|
{ ! isMobileViewport && areas.content && (
|
||||||
|
<div
|
||||||
|
className="edit-site-layout__area"
|
||||||
|
style={ {
|
||||||
|
maxWidth: widths?.content,
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ areas.content }
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ ! isMobileViewport && areas.edit && (
|
||||||
|
<div
|
||||||
|
className="edit-site-layout__area"
|
||||||
|
style={ {
|
||||||
|
maxWidth: widths?.edit,
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ areas.edit }
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,343 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Action, DataViews, View } from '@wordpress/dataviews';
|
||||||
|
import {
|
||||||
|
createElement,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from '@wordpress/element';
|
||||||
|
import { Product, ProductQuery } from '@woocommerce/data';
|
||||||
|
import { drawerRight } from '@wordpress/icons';
|
||||||
|
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
// @ts-expect-error missing types.
|
||||||
|
__experimentalHeading as Heading,
|
||||||
|
// @ts-expect-error missing types.
|
||||||
|
__experimentalText as Text,
|
||||||
|
// @ts-expect-error missing types.
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
// @ts-expect-error missing types.
|
||||||
|
__experimentalVStack as VStack,
|
||||||
|
FlexItem,
|
||||||
|
Button,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
|
import { privateApis as editorPrivateApis } from '@wordpress/editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { unlock } from '../../lock-unlock';
|
||||||
|
import {
|
||||||
|
useDefaultViews,
|
||||||
|
defaultLayouts,
|
||||||
|
} from '../sidebar-dataviews/default-views';
|
||||||
|
import { LAYOUT_LIST, OPERATOR_IS } from '../constants';
|
||||||
|
|
||||||
|
const { NavigableRegion } = unlock( editorPrivateApis );
|
||||||
|
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||||
|
|
||||||
|
const STATUSES = [
|
||||||
|
{ value: 'draft', label: __( 'Draft', 'woocommerce' ) },
|
||||||
|
{ value: 'future', label: __( 'Scheduled', 'woocommerce' ) },
|
||||||
|
{ value: 'private', label: __( 'Private', 'woocommerce' ) },
|
||||||
|
{ value: 'publish', label: __( 'Published', 'woocommerce' ) },
|
||||||
|
{ value: 'trash', label: __( 'Trash', 'woocommerce' ) },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: auto convert some of the product editor blocks ( from the blocks directory ) to this format.
|
||||||
|
* The edit function should work relatively well with the edit from the blocks, the only difference is that the blocks rely on getEntityProp to get the value
|
||||||
|
*/
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
label: __( 'Name', 'woocommerce' ),
|
||||||
|
enableHiding: false,
|
||||||
|
type: 'text',
|
||||||
|
render: function nameRender( { item }: { item: Product } ) {
|
||||||
|
return item.name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sku',
|
||||||
|
label: __( 'SKU', 'woocommerce' ),
|
||||||
|
enableHiding: false,
|
||||||
|
enableSorting: false,
|
||||||
|
render: ( { item }: { item: Product } ) => {
|
||||||
|
return item.sku;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'date',
|
||||||
|
label: __( 'Date', 'woocommerce' ),
|
||||||
|
render: ( { item }: { item: Product } ) => {
|
||||||
|
return <time>{ item.date_created }</time>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Status', 'woocommerce' ),
|
||||||
|
id: 'status',
|
||||||
|
getValue: ( { item }: { item: Product } ) =>
|
||||||
|
STATUSES.find( ( { value } ) => value === item.status )?.label ??
|
||||||
|
item.status,
|
||||||
|
elements: STATUSES,
|
||||||
|
filterBy: {
|
||||||
|
operators: [ OPERATOR_IS ],
|
||||||
|
},
|
||||||
|
enableSorting: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ProductListProps = {
|
||||||
|
subTitle?: string;
|
||||||
|
className?: string;
|
||||||
|
hideTitleFromUI?: boolean;
|
||||||
|
postType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAGE_SIZE = 25;
|
||||||
|
const EMPTY_ARRAY: Product[] = [];
|
||||||
|
const EMPTY_ACTIONS_ARRAY: Action< Product >[] = [];
|
||||||
|
|
||||||
|
const getDefaultView = (
|
||||||
|
defaultViews: Array< { slug: string; view: View } >,
|
||||||
|
activeView: string
|
||||||
|
) => {
|
||||||
|
return defaultViews.find( ( { slug } ) => slug === activeView )?.view;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function abstracts working with default & custom views by
|
||||||
|
* providing a [ state, setState ] tuple based on the URL parameters.
|
||||||
|
*
|
||||||
|
* Consumers use the provided tuple to work with state
|
||||||
|
* and don't have to deal with the specifics of default & custom views.
|
||||||
|
*
|
||||||
|
* @param {string} postType Post type to retrieve default views for.
|
||||||
|
* @return {Array} The [ state, setState ] tuple.
|
||||||
|
*/
|
||||||
|
function useView(
|
||||||
|
postType: string
|
||||||
|
): [ View, ( view: View ) => void, ( view: View ) => void ] {
|
||||||
|
const {
|
||||||
|
params: { activeView = 'all', isCustom = 'false', layout },
|
||||||
|
} = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const defaultViews = useDefaultViews( { postType } );
|
||||||
|
const [ view, setView ] = useState< View >( () => {
|
||||||
|
const initialView = getDefaultView( defaultViews, activeView ) ?? {
|
||||||
|
type: layout ?? LAYOUT_LIST,
|
||||||
|
};
|
||||||
|
|
||||||
|
const type = layout ?? initialView.type;
|
||||||
|
return {
|
||||||
|
...initialView,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
const setViewWithUrlUpdate = useCallback(
|
||||||
|
( newView: View ) => {
|
||||||
|
const { params } = history.getLocationWithParams();
|
||||||
|
|
||||||
|
if ( newView.type === LAYOUT_LIST && ! params?.layout ) {
|
||||||
|
// Skip updating the layout URL param if
|
||||||
|
// it is not present and the newView.type is LAYOUT_LIST.
|
||||||
|
} else if ( newView.type !== params?.layout ) {
|
||||||
|
history.push( {
|
||||||
|
...params,
|
||||||
|
layout: newView.type,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
setView( newView );
|
||||||
|
},
|
||||||
|
[ history, isCustom ]
|
||||||
|
);
|
||||||
|
|
||||||
|
// When layout URL param changes, update the view type
|
||||||
|
// without affecting any other config.
|
||||||
|
useEffect( () => {
|
||||||
|
setView( ( prevView ) => ( {
|
||||||
|
...prevView,
|
||||||
|
type: layout ?? LAYOUT_LIST,
|
||||||
|
} ) );
|
||||||
|
}, [ layout ] );
|
||||||
|
|
||||||
|
// When activeView or isCustom URL parameters change, reset the view.
|
||||||
|
useEffect( () => {
|
||||||
|
const newView = getDefaultView( defaultViews, activeView );
|
||||||
|
|
||||||
|
if ( newView ) {
|
||||||
|
const type = layout ?? newView.type;
|
||||||
|
setView( {
|
||||||
|
...newView,
|
||||||
|
type,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}, [ activeView, isCustom, layout, defaultViews ] );
|
||||||
|
|
||||||
|
return [ view, setViewWithUrlUpdate, setViewWithUrlUpdate ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemId( item: Product ) {
|
||||||
|
return item.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductList( {
|
||||||
|
subTitle,
|
||||||
|
className,
|
||||||
|
hideTitleFromUI = false,
|
||||||
|
}: ProductListProps ) {
|
||||||
|
const history = useHistory();
|
||||||
|
const location = useLocation();
|
||||||
|
const {
|
||||||
|
postId,
|
||||||
|
quickEdit = false,
|
||||||
|
postType = 'product',
|
||||||
|
isCustom,
|
||||||
|
activeView = 'all',
|
||||||
|
} = location.params;
|
||||||
|
const [ selection, setSelection ] = useState( [ postId ] );
|
||||||
|
const [ view, setView ] = useView( postType );
|
||||||
|
|
||||||
|
const queryParams = useMemo( () => {
|
||||||
|
const filters: Partial< ProductQuery > = {};
|
||||||
|
view.filters?.forEach( ( filter ) => {
|
||||||
|
if ( filter.field === 'status' ) {
|
||||||
|
filters.status = Array.isArray( filter.value )
|
||||||
|
? filter.value.join( ',' )
|
||||||
|
: filter.value;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
const orderby =
|
||||||
|
view.sort?.field === 'name' ? 'title' : view.sort?.field;
|
||||||
|
|
||||||
|
return {
|
||||||
|
per_page: view.perPage,
|
||||||
|
page: view.page,
|
||||||
|
order: view.sort?.direction,
|
||||||
|
orderby,
|
||||||
|
search: view.search,
|
||||||
|
...filters,
|
||||||
|
};
|
||||||
|
}, [ location.params, view ] );
|
||||||
|
|
||||||
|
const onChangeSelection = useCallback(
|
||||||
|
( items ) => {
|
||||||
|
setSelection( items );
|
||||||
|
history.push( {
|
||||||
|
...location.params,
|
||||||
|
postId: items.join( ',' ),
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
[ history, location.params, view?.type ]
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Use the Woo data store to get all the products, as this doesn't contain all the product data.
|
||||||
|
const { records, totalCount, isLoading } = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { getProducts, getProductsTotalCount, isResolving } =
|
||||||
|
select( 'wc/admin/products' );
|
||||||
|
return {
|
||||||
|
records: getProducts( queryParams ) as Product[],
|
||||||
|
totalCount: getProductsTotalCount( queryParams ) as number,
|
||||||
|
isLoading: isResolving( 'getProducts', [ queryParams ] ),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ queryParams ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const paginationInfo = useMemo(
|
||||||
|
() => ( {
|
||||||
|
totalItems: totalCount,
|
||||||
|
totalPages: Math.ceil( totalCount / ( view.perPage || PAGE_SIZE ) ),
|
||||||
|
} ),
|
||||||
|
[ totalCount, view.perPage ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const classes = classNames( 'edit-site-page', className );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigableRegion
|
||||||
|
className={ classes }
|
||||||
|
ariaLabel={ __( 'Products', 'woocommerce' ) }
|
||||||
|
>
|
||||||
|
<div className="edit-site-page-content">
|
||||||
|
{ ! hideTitleFromUI && (
|
||||||
|
<VStack
|
||||||
|
className="edit-site-page-header"
|
||||||
|
as="header"
|
||||||
|
spacing={ 0 }
|
||||||
|
>
|
||||||
|
<HStack className="edit-site-page-header__page-title">
|
||||||
|
<Heading
|
||||||
|
as="h2"
|
||||||
|
level={ 3 }
|
||||||
|
weight={ 500 }
|
||||||
|
className="edit-site-page-header__title"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
|
{ __( 'Products', 'woocommerce' ) }
|
||||||
|
</Heading>
|
||||||
|
<FlexItem className="edit-site-page-header__actions">
|
||||||
|
{ /* { actions } */ }
|
||||||
|
</FlexItem>
|
||||||
|
</HStack>
|
||||||
|
{ subTitle && (
|
||||||
|
<Text
|
||||||
|
variant="muted"
|
||||||
|
as="p"
|
||||||
|
className="edit-site-page-header__sub-title"
|
||||||
|
>
|
||||||
|
{ subTitle }
|
||||||
|
</Text>
|
||||||
|
) }
|
||||||
|
</VStack>
|
||||||
|
) }
|
||||||
|
<DataViews
|
||||||
|
key={ activeView + isCustom }
|
||||||
|
paginationInfo={ paginationInfo }
|
||||||
|
// @ts-expect-error types seem rather strict for this still.
|
||||||
|
fields={ fields }
|
||||||
|
actions={ EMPTY_ACTIONS_ARRAY }
|
||||||
|
data={ records || EMPTY_ARRAY }
|
||||||
|
isLoading={ isLoading }
|
||||||
|
view={ view }
|
||||||
|
onChangeView={ setView }
|
||||||
|
onChangeSelection={ onChangeSelection }
|
||||||
|
getItemId={ getItemId }
|
||||||
|
selection={ selection }
|
||||||
|
defaultLayouts={ defaultLayouts }
|
||||||
|
header={
|
||||||
|
<Button
|
||||||
|
// @ts-expect-error outdated type.
|
||||||
|
size="compact"
|
||||||
|
isPressed={ quickEdit }
|
||||||
|
icon={ drawerRight }
|
||||||
|
label={ __(
|
||||||
|
'Toggle details panel',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
onClick={ () => {
|
||||||
|
history.push( {
|
||||||
|
...location.params,
|
||||||
|
quickEdit: quickEdit ? undefined : true,
|
||||||
|
} );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</NavigableRegion>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { unlock } from '../lock-unlock';
|
||||||
|
import ProductList from './product-list';
|
||||||
|
import DataViewsSidebarContent from './sidebar-dataviews';
|
||||||
|
import SidebarNavigationScreen from './sidebar-navigation-screen';
|
||||||
|
|
||||||
|
const { useLocation } = unlock( routerPrivateApis );
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
key: string;
|
||||||
|
areas: {
|
||||||
|
sidebar: React.JSX.Element | React.FunctionComponent;
|
||||||
|
content?: React.JSX.Element | React.FunctionComponent;
|
||||||
|
edit?: React.JSX.Element | React.FunctionComponent;
|
||||||
|
mobile?: React.JSX.Element | React.FunctionComponent | boolean;
|
||||||
|
preview?: boolean;
|
||||||
|
};
|
||||||
|
widths?: {
|
||||||
|
content?: number;
|
||||||
|
edit?: number;
|
||||||
|
sidebar?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useLayoutAreas() {
|
||||||
|
const { params = {} } = useLocation();
|
||||||
|
const { postType = 'product', layout = 'table', canvas } = params;
|
||||||
|
// Products list.
|
||||||
|
if ( [ 'product' ].includes( postType ) ) {
|
||||||
|
const isListLayout = layout === 'list' || ! layout;
|
||||||
|
return {
|
||||||
|
key: 'products-list',
|
||||||
|
areas: {
|
||||||
|
sidebar: (
|
||||||
|
<SidebarNavigationScreen
|
||||||
|
title={ 'Products' }
|
||||||
|
isRoot
|
||||||
|
content={ <DataViewsSidebarContent /> }
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
content: <ProductList />,
|
||||||
|
preview: false,
|
||||||
|
mobile: <ProductList postType={ postType } />,
|
||||||
|
},
|
||||||
|
widths: {
|
||||||
|
content: isListLayout ? 380 : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback shows the home page preview
|
||||||
|
return {
|
||||||
|
key: 'default',
|
||||||
|
areas: {
|
||||||
|
sidebar: () => null,
|
||||||
|
preview: false,
|
||||||
|
mobile: canvas === 'edit',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||||
|
import { addQueryArgs, getQueryArgs, removeQueryArgs } from '@wordpress/url';
|
||||||
|
import { VIEW_LAYOUTS } from '@wordpress/dataviews';
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
|
import { __experimentalHStack as HStack } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import SidebarNavigationItem from '../sidebar-navigation-item';
|
||||||
|
import { unlock } from '../../lock-unlock';
|
||||||
|
|
||||||
|
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||||
|
|
||||||
|
type DataViewItemProps = {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
customViewId?: string;
|
||||||
|
type: string;
|
||||||
|
icon: React.JSX.Element;
|
||||||
|
isActive: boolean;
|
||||||
|
isCustom: boolean;
|
||||||
|
suffix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useLink(
|
||||||
|
params: Record< string, string | undefined >,
|
||||||
|
state?: Record< string, string | undefined >,
|
||||||
|
shouldReplace = false
|
||||||
|
) {
|
||||||
|
const history = useHistory();
|
||||||
|
function onClick( event: Event ) {
|
||||||
|
event?.preventDefault();
|
||||||
|
|
||||||
|
if ( shouldReplace ) {
|
||||||
|
history.replace( params, state );
|
||||||
|
} else {
|
||||||
|
history.push( params, state );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentArgs = getQueryArgs( window.location.href );
|
||||||
|
const currentUrlWithoutArgs = removeQueryArgs(
|
||||||
|
window.location.href,
|
||||||
|
...Object.keys( currentArgs )
|
||||||
|
);
|
||||||
|
|
||||||
|
const newUrl = addQueryArgs( currentUrlWithoutArgs, params );
|
||||||
|
|
||||||
|
return {
|
||||||
|
href: newUrl,
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DataViewItem( {
|
||||||
|
title,
|
||||||
|
slug,
|
||||||
|
customViewId,
|
||||||
|
type,
|
||||||
|
icon,
|
||||||
|
isActive,
|
||||||
|
isCustom,
|
||||||
|
suffix,
|
||||||
|
}: DataViewItemProps ) {
|
||||||
|
const {
|
||||||
|
params: { postType, page },
|
||||||
|
} = useLocation();
|
||||||
|
|
||||||
|
const iconToUse =
|
||||||
|
icon || VIEW_LAYOUTS.find( ( v ) => v.type === type )?.icon;
|
||||||
|
|
||||||
|
let activeView: undefined | string = isCustom ? customViewId : slug;
|
||||||
|
if ( activeView === 'all' ) {
|
||||||
|
activeView = undefined;
|
||||||
|
}
|
||||||
|
const linkInfo = useLink( {
|
||||||
|
page,
|
||||||
|
postType,
|
||||||
|
layout: type,
|
||||||
|
activeView,
|
||||||
|
isCustom: isCustom ? 'true' : undefined,
|
||||||
|
} );
|
||||||
|
return (
|
||||||
|
<HStack
|
||||||
|
justify="flex-start"
|
||||||
|
className={ classNames(
|
||||||
|
'edit-site-sidebar-dataviews-dataview-item',
|
||||||
|
{
|
||||||
|
'is-selected': isActive,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<SidebarNavigationItem
|
||||||
|
icon={ iconToUse }
|
||||||
|
{ ...linkInfo }
|
||||||
|
aria-current={ isActive ? 'true' : undefined }
|
||||||
|
>
|
||||||
|
{ title }
|
||||||
|
</SidebarNavigationItem>
|
||||||
|
{ suffix }
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { store as coreStore } from '@wordpress/core-data';
|
||||||
|
import { useMemo } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
trash,
|
||||||
|
pages,
|
||||||
|
drafts,
|
||||||
|
published,
|
||||||
|
scheduled,
|
||||||
|
notAllowed,
|
||||||
|
} from '@wordpress/icons';
|
||||||
|
import type { ColumnStyle, ViewTable } from '@wordpress/dataviews';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
LAYOUT_LIST,
|
||||||
|
LAYOUT_TABLE,
|
||||||
|
LAYOUT_GRID,
|
||||||
|
OPERATOR_IS,
|
||||||
|
} from '../constants';
|
||||||
|
|
||||||
|
export const defaultLayouts: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
layout: {
|
||||||
|
primaryField: string;
|
||||||
|
mediaField?: string;
|
||||||
|
styles?: Record< string, ColumnStyle >;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
[ LAYOUT_TABLE ]: {
|
||||||
|
layout: {
|
||||||
|
primaryField: 'name',
|
||||||
|
styles: {
|
||||||
|
name: {
|
||||||
|
maxWidth: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[ LAYOUT_GRID ]: {
|
||||||
|
layout: {
|
||||||
|
mediaField: 'featured-image',
|
||||||
|
primaryField: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[ LAYOUT_LIST ]: {
|
||||||
|
layout: {
|
||||||
|
primaryField: 'name',
|
||||||
|
mediaField: 'featured-image',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_POST_BASE: Omit< ViewTable, 'view' | 'title' | 'slug' | 'icon' > =
|
||||||
|
{
|
||||||
|
type: LAYOUT_TABLE,
|
||||||
|
search: '',
|
||||||
|
filters: [],
|
||||||
|
page: 1,
|
||||||
|
perPage: 20,
|
||||||
|
sort: {
|
||||||
|
field: 'date',
|
||||||
|
direction: 'desc',
|
||||||
|
},
|
||||||
|
fields: [ 'name', 'sku', 'status', 'date' ],
|
||||||
|
layout: defaultLayouts[ LAYOUT_LIST ].layout,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDefaultViews( { postType }: { postType: string } ): Array< {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
icon: React.JSX.Element;
|
||||||
|
view: ViewTable;
|
||||||
|
} > {
|
||||||
|
const labels = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { getPostType } = select( coreStore );
|
||||||
|
const postTypeData: { labels?: Record< string, string > } =
|
||||||
|
getPostType( postType );
|
||||||
|
return postTypeData?.labels;
|
||||||
|
},
|
||||||
|
[ postType ]
|
||||||
|
);
|
||||||
|
return useMemo( () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: labels?.all_items || __( 'All items', 'woocommerce' ),
|
||||||
|
slug: 'all',
|
||||||
|
icon: pages,
|
||||||
|
view: { ...DEFAULT_POST_BASE },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: __( 'Published', 'woocommerce' ),
|
||||||
|
slug: 'published',
|
||||||
|
icon: published,
|
||||||
|
view: {
|
||||||
|
...DEFAULT_POST_BASE,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
operator: OPERATOR_IS,
|
||||||
|
value: 'publish',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: __( 'Scheduled', 'woocommerce' ),
|
||||||
|
slug: 'future',
|
||||||
|
icon: scheduled,
|
||||||
|
view: {
|
||||||
|
...DEFAULT_POST_BASE,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
operator: OPERATOR_IS,
|
||||||
|
value: 'future',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: __( 'Drafts', 'woocommerce' ),
|
||||||
|
slug: 'drafts',
|
||||||
|
icon: drafts,
|
||||||
|
view: {
|
||||||
|
...DEFAULT_POST_BASE,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
operator: OPERATOR_IS,
|
||||||
|
value: 'draft',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: __( 'Private', 'woocommerce' ),
|
||||||
|
slug: 'private',
|
||||||
|
icon: notAllowed,
|
||||||
|
view: {
|
||||||
|
...DEFAULT_POST_BASE,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
operator: OPERATOR_IS,
|
||||||
|
value: 'private',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: __( 'Trash', 'woocommerce' ),
|
||||||
|
slug: 'trash',
|
||||||
|
icon: trash,
|
||||||
|
view: {
|
||||||
|
...DEFAULT_POST_BASE,
|
||||||
|
type: LAYOUT_TABLE,
|
||||||
|
layout: defaultLayouts[ LAYOUT_TABLE ].layout,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
operator: OPERATOR_IS,
|
||||||
|
value: 'trash',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [ labels ] );
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement, Fragment } from '@wordpress/element';
|
||||||
|
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis, @woocommerce/dependency-group
|
||||||
|
import { __experimentalItemGroup as ItemGroup } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { unlock } from '../../lock-unlock';
|
||||||
|
import DataViewItem from './dataview-item';
|
||||||
|
import { useDefaultViews } from './default-views';
|
||||||
|
|
||||||
|
const { useLocation } = unlock( routerPrivateApis );
|
||||||
|
|
||||||
|
export default function DataViewsSidebarContent() {
|
||||||
|
const {
|
||||||
|
params: {
|
||||||
|
postType = 'product',
|
||||||
|
activeView = 'all',
|
||||||
|
isCustom = 'false',
|
||||||
|
},
|
||||||
|
} = useLocation();
|
||||||
|
const defaultViews = useDefaultViews( { postType } );
|
||||||
|
if ( ! postType ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isCustomBoolean = isCustom === 'true';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ItemGroup>
|
||||||
|
{ defaultViews.map( ( dataview ) => {
|
||||||
|
return (
|
||||||
|
<DataViewItem
|
||||||
|
key={ dataview.slug }
|
||||||
|
slug={ dataview.slug }
|
||||||
|
title={ dataview.title }
|
||||||
|
icon={ dataview.icon }
|
||||||
|
type={ dataview.view.type }
|
||||||
|
isActive={
|
||||||
|
! isCustomBoolean &&
|
||||||
|
dataview.slug === activeView
|
||||||
|
}
|
||||||
|
isCustom={ false }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</ItemGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
.edit-site-sidebar-navigation-screen__title-icon {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: $gray-900;
|
||||||
|
padding-top: $grid-unit-60;
|
||||||
|
margin-bottom: $grid-unit-10;
|
||||||
|
padding-bottom: $grid-unit-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-site-sidebar-button {
|
||||||
|
color: #e0e0e0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-site-sidebar-navigation-screen__title {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
padding: 2px 0 0
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isRTL } from '@wordpress/i18n';
|
||||||
|
import { chevronRightSmall, chevronLeftSmall, Icon } from '@wordpress/icons';
|
||||||
|
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__experimentalItem as Item,
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
FlexBlock,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { unlock } from '../../lock-unlock';
|
||||||
|
|
||||||
|
const { useHistory } = unlock( routerPrivateApis );
|
||||||
|
|
||||||
|
type SidebarNavigationItemProps = {
|
||||||
|
className?: string;
|
||||||
|
icon?: React.JSX.Element;
|
||||||
|
suffix?: string;
|
||||||
|
withChevron?: boolean;
|
||||||
|
uid?: string;
|
||||||
|
params?: Record< string, string >;
|
||||||
|
onClick?: ( e: Event ) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SidebarNavigationItem( {
|
||||||
|
className,
|
||||||
|
icon,
|
||||||
|
withChevron = false,
|
||||||
|
suffix,
|
||||||
|
uid,
|
||||||
|
params,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: SidebarNavigationItemProps ) {
|
||||||
|
const history = useHistory();
|
||||||
|
// If there is no custom click handler, create one that navigates to `params`.
|
||||||
|
function handleClick( e: Event ) {
|
||||||
|
if ( onClick ) {
|
||||||
|
onClick( e );
|
||||||
|
} else if ( params ) {
|
||||||
|
e.preventDefault();
|
||||||
|
history.push( params );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Item
|
||||||
|
className={ classNames(
|
||||||
|
'edit-site-sidebar-navigation-item',
|
||||||
|
{ 'with-suffix': ! withChevron && suffix },
|
||||||
|
className
|
||||||
|
) }
|
||||||
|
onClick={ handleClick }
|
||||||
|
id={ uid }
|
||||||
|
{ ...props }
|
||||||
|
>
|
||||||
|
<HStack justify="flex-start">
|
||||||
|
{ icon && (
|
||||||
|
<Icon
|
||||||
|
style={ { fill: 'currentcolor' } }
|
||||||
|
icon={ icon }
|
||||||
|
size={ 24 }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<FlexBlock>{ children }</FlexBlock>
|
||||||
|
{ withChevron && (
|
||||||
|
<Icon
|
||||||
|
icon={ isRTL() ? chevronLeftSmall : chevronRightSmall }
|
||||||
|
className="edit-site-sidebar-navigation-item__drilldown-indicator"
|
||||||
|
size={ 24 }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
{ ! withChevron && suffix }
|
||||||
|
</HStack>
|
||||||
|
</Item>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isRTL, __ } from '@wordpress/i18n';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { chevronRight, chevronLeft } from '@wordpress/icons';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { privateApis as routerPrivateApis } from '@wordpress/router';
|
||||||
|
import { createElement, Fragment } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__experimentalHeading as Heading,
|
||||||
|
// @ts-expect-error missing type.
|
||||||
|
__experimentalVStack as VStack,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { unlock } from '../../lock-unlock';
|
||||||
|
import SidebarButton from './sidebar-button';
|
||||||
|
|
||||||
|
const { useHistory, useLocation } = unlock( routerPrivateApis );
|
||||||
|
|
||||||
|
type SidebarNavigationScreenProps = {
|
||||||
|
isRoot?: boolean;
|
||||||
|
title: string;
|
||||||
|
actions?: React.JSX.Element;
|
||||||
|
meta?: string;
|
||||||
|
content: React.JSX.Element;
|
||||||
|
footer?: string;
|
||||||
|
description?: string;
|
||||||
|
backPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SidebarNavigationScreen( {
|
||||||
|
isRoot,
|
||||||
|
title,
|
||||||
|
actions,
|
||||||
|
meta,
|
||||||
|
content,
|
||||||
|
footer,
|
||||||
|
description,
|
||||||
|
backPath: backPathProp,
|
||||||
|
}: SidebarNavigationScreenProps ) {
|
||||||
|
const { dashboardLink, dashboardLinkText } = useSelect( ( select ) => {
|
||||||
|
const { getSettings } = unlock( select( 'core/edit-site' ) );
|
||||||
|
return {
|
||||||
|
dashboardLink: getSettings().__experimentalDashboardLink,
|
||||||
|
dashboardLinkText: getSettings().__experimentalDashboardLinkText,
|
||||||
|
};
|
||||||
|
}, [] );
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
const backPath = backPathProp ?? location.state?.backPath;
|
||||||
|
const icon = isRTL() ? chevronRight : chevronLeft;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VStack
|
||||||
|
className={ classNames(
|
||||||
|
'edit-site-sidebar-navigation-screen__main',
|
||||||
|
{
|
||||||
|
'has-footer': !! footer,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
spacing={ 0 }
|
||||||
|
justify="flex-start"
|
||||||
|
>
|
||||||
|
<HStack
|
||||||
|
spacing={ 3 }
|
||||||
|
alignment="flex-start"
|
||||||
|
className="edit-site-sidebar-navigation-screen__title-icon"
|
||||||
|
>
|
||||||
|
{ ! isRoot && (
|
||||||
|
<SidebarButton
|
||||||
|
onClick={ () => {
|
||||||
|
history.push( backPath );
|
||||||
|
} }
|
||||||
|
icon={ icon }
|
||||||
|
label={ __( 'Back', 'woocommerce' ) }
|
||||||
|
showTooltip={ false }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
{ isRoot && (
|
||||||
|
<SidebarButton
|
||||||
|
icon={ icon }
|
||||||
|
label={
|
||||||
|
dashboardLinkText ||
|
||||||
|
__( 'Go to the Dashboard', 'woocommerce' )
|
||||||
|
}
|
||||||
|
href={ dashboardLink || 'index.php' }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<Heading
|
||||||
|
className="edit-site-sidebar-navigation-screen__title"
|
||||||
|
color={ '#e0e0e0' /* $gray-200 */ }
|
||||||
|
level={ 1 }
|
||||||
|
size={ 20 }
|
||||||
|
>
|
||||||
|
{ title }
|
||||||
|
</Heading>
|
||||||
|
{ actions && (
|
||||||
|
<div className="edit-site-sidebar-navigation-screen__actions">
|
||||||
|
{ actions }
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</HStack>
|
||||||
|
{ meta && (
|
||||||
|
<>
|
||||||
|
<div className="edit-site-sidebar-navigation-screen__meta">
|
||||||
|
{ meta }
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) }
|
||||||
|
|
||||||
|
<div className="edit-site-sidebar-navigation-screen__content">
|
||||||
|
{ description && (
|
||||||
|
<p className="edit-site-sidebar-navigation-screen__description">
|
||||||
|
{ description }
|
||||||
|
</p>
|
||||||
|
) }
|
||||||
|
{ content }
|
||||||
|
</div>
|
||||||
|
</VStack>
|
||||||
|
{ footer && (
|
||||||
|
<footer className="edit-site-sidebar-navigation-screen__footer">
|
||||||
|
{ footer }
|
||||||
|
</footer>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default function SidebarButton( props: Button.Props ) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{ ...props }
|
||||||
|
className={ classNames(
|
||||||
|
'edit-site-sidebar-button',
|
||||||
|
props.className
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement, useRef } from '@wordpress/element';
|
||||||
|
|
||||||
|
function SidebarContentWrapper( { children }: { children: React.ReactNode } ) {
|
||||||
|
const wrapperRef = useRef< HTMLDivElement | null >( null );
|
||||||
|
const wrapperCls = 'edit-site-sidebar__screen-wrapper';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ wrapperRef } className={ wrapperCls }>
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidebarContent( {
|
||||||
|
routeKey,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
routeKey: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
} ) {
|
||||||
|
return (
|
||||||
|
<div className="edit-site-sidebar__content">
|
||||||
|
<SidebarContentWrapper key={ routeKey }>
|
||||||
|
{ children }
|
||||||
|
</SidebarContentWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement, memo, forwardRef } from '@wordpress/element';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { store as coreStore } from '@wordpress/core-data';
|
||||||
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
import { filterURLForDisplay } from '@wordpress/url';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
// @ts-expect-error missing types.
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
VisuallyHidden,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import SiteIcon from './site-icon';
|
||||||
|
import { unlock } from '../../lock-unlock';
|
||||||
|
|
||||||
|
const SiteHub = memo(
|
||||||
|
forwardRef(
|
||||||
|
(
|
||||||
|
{ isTransparent }: { isTransparent: boolean },
|
||||||
|
ref: React.Ref< HTMLAnchorElement >
|
||||||
|
) => {
|
||||||
|
const { dashboardLink, homeUrl, siteTitle } = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { getSettings } = unlock(
|
||||||
|
select( 'core/edit-site' )
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getSite,
|
||||||
|
getUnstableBase, // Site index.
|
||||||
|
} = select( coreStore );
|
||||||
|
const _site: undefined | { title: string; url: string } =
|
||||||
|
getSite();
|
||||||
|
|
||||||
|
const base: { home: string } | undefined =
|
||||||
|
getUnstableBase();
|
||||||
|
return {
|
||||||
|
dashboardLink:
|
||||||
|
getSettings().__experimentalDashboardLink ||
|
||||||
|
'index.php',
|
||||||
|
homeUrl: base?.home,
|
||||||
|
siteTitle:
|
||||||
|
! _site?.title && !! _site?.url
|
||||||
|
? filterURLForDisplay( _site?.url )
|
||||||
|
: _site?.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="edit-site-site-hub">
|
||||||
|
<HStack justify="flex-start" spacing="0">
|
||||||
|
<div
|
||||||
|
className={ classNames(
|
||||||
|
'edit-site-site-hub__view-mode-toggle-container',
|
||||||
|
{
|
||||||
|
'has-transparent-background': isTransparent,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
ref={ ref }
|
||||||
|
href={ dashboardLink }
|
||||||
|
label={ __(
|
||||||
|
'Go to the Dashboard',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
className="edit-site-layout__view-mode-toggle"
|
||||||
|
style={ {
|
||||||
|
transform: 'scale(0.5)',
|
||||||
|
borderRadius: 4,
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<SiteIcon className="edit-site-layout__view-mode-toggle-icon" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HStack>
|
||||||
|
<div className="edit-site-site-hub__title">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
href={ homeUrl }
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{ siteTitle && decodeEntities( siteTitle ) }
|
||||||
|
<VisuallyHidden as="span">
|
||||||
|
{
|
||||||
|
/* translators: accessibility text */
|
||||||
|
__(
|
||||||
|
'(opens in a new tab)',
|
||||||
|
'woocommerce'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</VisuallyHidden>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SiteHub;
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { Icon } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { wordpress } from '@wordpress/icons';
|
||||||
|
import { store as coreDataStore } from '@wordpress/core-data';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
type SiteIconProps = {
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SiteIcon( { className }: SiteIconProps ) {
|
||||||
|
const { isRequestingSite, siteIconUrl } = useSelect( ( select ) => {
|
||||||
|
const { getEntityRecord } = select( coreDataStore );
|
||||||
|
const siteData: { site_icon_url?: string } = getEntityRecord(
|
||||||
|
'root',
|
||||||
|
'__unstableBase',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRequestingSite: ! siteData,
|
||||||
|
siteIconUrl: siteData?.site_icon_url,
|
||||||
|
};
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
if ( isRequestingSite && ! siteIconUrl ) {
|
||||||
|
return <div className="edit-site-site-icon__image" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = siteIconUrl ? (
|
||||||
|
<img
|
||||||
|
className="edit-site-site-icon__image"
|
||||||
|
alt={ __( 'Site Icon', 'woocommerce' ) }
|
||||||
|
src={ siteIconUrl }
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
className="edit-site-site-icon__icon"
|
||||||
|
icon={ wordpress }
|
||||||
|
size={ 48 }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ classNames( className, 'edit-site-site-icon' ) }>
|
||||||
|
{ icon }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SiteIcon;
|
|
@ -4,11 +4,12 @@
|
||||||
.woocommerce_page_woocommerce-products-dashboard #adminmenumain {
|
.woocommerce_page_woocommerce-products-dashboard #adminmenumain {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce_page_woocommerce-products-dashboard #wpcontent {
|
.woocommerce_page_woocommerce-products-dashboard #wpcontent {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
body.woocommerce_page_woocommerce-products-dashboard
|
|
||||||
#woocommerce-products-dashboard {
|
body.woocommerce_page_woocommerce-products-dashboard #woocommerce-products-dashboard {
|
||||||
@include wp-admin-reset("#woocommerce-products-dashboard");
|
@include wp-admin-reset("#woocommerce-products-dashboard");
|
||||||
@include reset;
|
@include reset;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
@ -38,3 +39,5 @@ body.js.is-fullscreen-mode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import "products-app/sidebar-dataviews/style.scss";
|
||||||
|
|
|
@ -31,6 +31,7 @@ declare module '@wordpress/core-data' {
|
||||||
isResolving: boolean;
|
isResolving: boolean;
|
||||||
hasResolved: boolean;
|
hasResolved: boolean;
|
||||||
};
|
};
|
||||||
|
const store: string;
|
||||||
}
|
}
|
||||||
declare module '@wordpress/keyboard-shortcuts' {
|
declare module '@wordpress/keyboard-shortcuts' {
|
||||||
function useShortcut(
|
function useShortcut(
|
||||||
|
@ -43,3 +44,24 @@ declare module '@wordpress/keyboard-shortcuts' {
|
||||||
declare module '@wordpress/router' {
|
declare module '@wordpress/router' {
|
||||||
const privateApis;
|
const privateApis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@wordpress/edit-site/build-module/components/sync-state-with-url/use-init-edited-entity-from-url' {
|
||||||
|
export default function useInitEditedEntityFromURL(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@wordpress/edit-site/build-module/components/sidebar-navigation-screen' {
|
||||||
|
const SidebarNavigationScreen: React.FunctionComponent< {
|
||||||
|
title: string;
|
||||||
|
isRoot: boolean;
|
||||||
|
content: JSX.Element;
|
||||||
|
} >;
|
||||||
|
export default SidebarNavigationScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@wordpress/edit-site/build-module/components/site-hub' {
|
||||||
|
const SiteHub: React.FunctionComponent< {
|
||||||
|
ref: React.Ref;
|
||||||
|
isTransparent: boolean;
|
||||||
|
} >;
|
||||||
|
export default SiteHub;
|
||||||
|
}
|
||||||
|
|
|
@ -186,6 +186,8 @@ const webpackConfig = {
|
||||||
extensions: [ '.json', '.js', '.jsx', '.ts', '.tsx' ],
|
extensions: [ '.json', '.js', '.jsx', '.ts', '.tsx' ],
|
||||||
alias: {
|
alias: {
|
||||||
'~': path.resolve( __dirname + '/client' ),
|
'~': path.resolve( __dirname + '/client' ),
|
||||||
|
'react/jsx-dev-runtime': require.resolve( 'react/jsx-dev-runtime' ),
|
||||||
|
'react/jsx-runtime': require.resolve( 'react/jsx-runtime' ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -240,6 +242,10 @@ const webpackConfig = {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( request.startsWith( '@wordpress/dataviews' ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ( request.startsWith( '@wordpress/edit-site' ) ) {
|
if ( request.startsWith( '@wordpress/edit-site' ) ) {
|
||||||
// The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite.
|
// The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite.
|
||||||
// We use the edit-site components in the customize store.
|
// We use the edit-site components in the customize store.
|
||||||
|
|
|
@ -74,6 +74,7 @@ function toggle_remote_logging( $request ) {
|
||||||
update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' );
|
update_option( 'woocommerce_feature_remote_logging_enabled', 'yes' );
|
||||||
update_option( 'woocommerce_allow_tracking', 'yes' );
|
update_option( 'woocommerce_allow_tracking', 'yes' );
|
||||||
update_option( 'woocommerce_remote_variant_assignment', 1 );
|
update_option( 'woocommerce_remote_variant_assignment', 1 );
|
||||||
|
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, WC()->version );
|
||||||
} else {
|
} else {
|
||||||
update_option( 'woocommerce_feature_remote_logging_enabled', 'no' );
|
update_option( 'woocommerce_feature_remote_logging_enabled', 'no' );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update remote logger tool to toggle remote logging feature properly
|
|
@ -24,6 +24,14 @@ export const blockAttributes: BlockAttributes = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
prefix: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'SKU:',
|
||||||
|
},
|
||||||
|
suffix: {
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default blockAttributes;
|
export default blockAttributes;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
useInnerBlockLayoutContext,
|
useInnerBlockLayoutContext,
|
||||||
|
@ -10,6 +9,8 @@ import {
|
||||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||||
|
import { RichText } from '@wordpress/block-editor';
|
||||||
|
import type { BlockEditProps } from '@wordpress/blocks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -17,18 +18,24 @@ import { useStyleProps } from '@woocommerce/base-hooks';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import type { Attributes } from './types';
|
import type { Attributes } from './types';
|
||||||
|
|
||||||
type Props = Attributes & HTMLAttributes< HTMLDivElement >;
|
type Props = BlockEditProps< Attributes > & HTMLAttributes< HTMLDivElement >;
|
||||||
|
|
||||||
const Preview = ( {
|
const Preview = ( {
|
||||||
|
setAttributes,
|
||||||
parentClassName,
|
parentClassName,
|
||||||
sku,
|
sku,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
}: {
|
}: {
|
||||||
|
setAttributes: ( attributes: Record< string, unknown > ) => void;
|
||||||
parentClassName: string;
|
parentClassName: string;
|
||||||
sku: string;
|
sku: string;
|
||||||
className?: string | undefined;
|
className?: string | undefined;
|
||||||
style?: React.CSSProperties | undefined;
|
style?: React.CSSProperties | undefined;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
} ) => (
|
} ) => (
|
||||||
<div
|
<div
|
||||||
className={ clsx( className, {
|
className={ clsx( className, {
|
||||||
|
@ -36,7 +43,19 @@ const Preview = ( {
|
||||||
} ) }
|
} ) }
|
||||||
style={ style }
|
style={ style }
|
||||||
>
|
>
|
||||||
{ __( 'SKU:', 'woocommerce' ) } <strong>{ sku }</strong>
|
<RichText
|
||||||
|
tagName="span"
|
||||||
|
placeholder="Prefix"
|
||||||
|
value={ prefix }
|
||||||
|
onChange={ ( value ) => setAttributes( { prefix: value } ) }
|
||||||
|
/>
|
||||||
|
<strong>{ sku }</strong>
|
||||||
|
<RichText
|
||||||
|
tagName="span"
|
||||||
|
placeholder="Suffix"
|
||||||
|
value={ suffix }
|
||||||
|
onChange={ ( value ) => setAttributes( { suffix: value } ) }
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -50,9 +69,12 @@ const Block = ( props: Props ): JSX.Element | null => {
|
||||||
if ( props.isDescendentOfSingleProductTemplate ) {
|
if ( props.isDescendentOfSingleProductTemplate ) {
|
||||||
return (
|
return (
|
||||||
<Preview
|
<Preview
|
||||||
|
setAttributes={ props.setAttributes }
|
||||||
parentClassName={ parentClassName }
|
parentClassName={ parentClassName }
|
||||||
className={ className }
|
className={ className }
|
||||||
sku={ 'Product SKU' }
|
sku={ 'Product SKU' }
|
||||||
|
prefix={ props.prefix }
|
||||||
|
suffix={ props.suffix }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -63,9 +85,12 @@ const Block = ( props: Props ): JSX.Element | null => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Preview
|
<Preview
|
||||||
|
setAttributes={ props.setAttributes }
|
||||||
className={ className }
|
className={ className }
|
||||||
parentClassName={ parentClassName }
|
parentClassName={ parentClassName }
|
||||||
sku={ sku }
|
sku={ sku }
|
||||||
|
prefix={ props.prefix }
|
||||||
|
suffix={ props.suffix }
|
||||||
{ ...( props.isDescendantOfAllProducts && {
|
{ ...( props.isDescendantOfAllProducts && {
|
||||||
className: clsx(
|
className: clsx(
|
||||||
className,
|
className,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { useEffect } from '@wordpress/element';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import './editor.scss';
|
||||||
import Block from './block';
|
import Block from './block';
|
||||||
import type { Attributes } from './types';
|
import type { Attributes } from './types';
|
||||||
import { useIsDescendentOfSingleProductBlock } from '../shared/use-is-descendent-of-single-product-block';
|
import { useIsDescendentOfSingleProductBlock } from '../shared/use-is-descendent-of-single-product-block';
|
||||||
|
@ -68,7 +69,7 @@ const Edit = ( {
|
||||||
attributes.isDescendantOfAllProducts ? undefined : style
|
attributes.isDescendantOfAllProducts ? undefined : style
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Block { ...blockAttrs } />
|
<Block { ...blockAttrs } setAttributes={ setAttributes } />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.wc-block-components-product-sku strong {
|
||||||
|
margin-left: $gap-smallest;
|
||||||
|
margin-right: $gap-smallest;
|
||||||
|
}
|
|
@ -29,6 +29,9 @@ const blockConfig: BlockConfiguration = {
|
||||||
'woocommerce/product-meta',
|
'woocommerce/product-meta',
|
||||||
],
|
],
|
||||||
edit,
|
edit,
|
||||||
|
save() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
supports,
|
supports,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
.wc-block-components-product-sku {
|
.wc-block-components-product-sku {
|
||||||
display: block;
|
display: block;
|
||||||
text-transform: uppercase;
|
|
||||||
@include font-size(small);
|
@include font-size(small);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,6 @@ export interface Attributes {
|
||||||
isDescendentOfSingleProductBlock: boolean;
|
isDescendentOfSingleProductBlock: boolean;
|
||||||
showProductSelector: boolean;
|
showProductSelector: boolean;
|
||||||
isDescendantOfAllProducts: boolean;
|
isDescendantOfAllProducts: boolean;
|
||||||
|
prefix: string;
|
||||||
|
suffix: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
import { Icon, info } from '@wordpress/icons';
|
||||||
|
import ProductControl from '@woocommerce/editor-components/product-control';
|
||||||
|
import type { SelectedOption } from '@woocommerce/block-hocs';
|
||||||
|
import { createInterpolateElement } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
Placeholder,
|
||||||
|
// @ts-expect-error Using experimental features
|
||||||
|
__experimentalHStack as HStack,
|
||||||
|
// @ts-expect-error Using experimental features
|
||||||
|
__experimentalText as Text,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import type { ProductCollectionEditComponentProps } from '../types';
|
||||||
|
import { getCollectionByName } from '../collections';
|
||||||
|
|
||||||
|
const ProductPicker = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
|
const blockProps = useBlockProps();
|
||||||
|
const attributes = props.attributes;
|
||||||
|
|
||||||
|
const collection = getCollectionByName( attributes.collection );
|
||||||
|
if ( ! collection ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div { ...blockProps }>
|
||||||
|
<Placeholder className="wc-blocks-product-collection__editor-product-picker">
|
||||||
|
<HStack alignment="center">
|
||||||
|
<Icon
|
||||||
|
icon={ info }
|
||||||
|
className="wc-blocks-product-collection__info-icon"
|
||||||
|
/>
|
||||||
|
<Text>
|
||||||
|
{ createInterpolateElement(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: collection title */
|
||||||
|
__(
|
||||||
|
'<strong>%s</strong> requires a product to be selected in order to display associated items.',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
collection.title
|
||||||
|
),
|
||||||
|
{
|
||||||
|
strong: <strong />,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<ProductControl
|
||||||
|
selected={
|
||||||
|
attributes.query?.productReference as SelectedOption
|
||||||
|
}
|
||||||
|
onChange={ ( value = [] ) => {
|
||||||
|
const isValidId = ( value[ 0 ]?.id ?? null ) !== null;
|
||||||
|
if ( isValidId ) {
|
||||||
|
props.setAttributes( {
|
||||||
|
query: {
|
||||||
|
...attributes.query,
|
||||||
|
productReference: value[ 0 ].id,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
messages={ {
|
||||||
|
search: __( 'Select a product', 'woocommerce' ),
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
</Placeholder>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductPicker;
|
|
@ -168,3 +168,49 @@ $max-button-width: calc(100% / #{$max-button-columns});
|
||||||
color: var(--wp-components-color-accent-inverted, #fff);
|
color: var(--wp-components-color-accent-inverted, #fff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Editor Product Picker
|
||||||
|
.wc-blocks-product-collection__editor-product-picker {
|
||||||
|
.wc-blocks-product-collection__info-icon {
|
||||||
|
fill: var(--wp--preset--color--luminous-vivid-orange, #e26f56);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linked Product Control
|
||||||
|
.wc-block-product-collection-linked-product-control {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid $gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image-container {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-collection-linked-product__popover-content .components-popover__content {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.woocommerce-search-list__search {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,18 +4,25 @@
|
||||||
import { store as blockEditorStore } from '@wordpress/block-editor';
|
import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import type { ProductCollectionEditComponentProps } from '../types';
|
import {
|
||||||
|
ProductCollectionEditComponentProps,
|
||||||
|
ProductCollectionUIStatesInEditor,
|
||||||
|
} from '../types';
|
||||||
import ProductCollectionPlaceholder from './product-collection-placeholder';
|
import ProductCollectionPlaceholder from './product-collection-placeholder';
|
||||||
import ProductCollectionContent from './product-collection-content';
|
import ProductCollectionContent from './product-collection-content';
|
||||||
import CollectionSelectionModal from './collection-selection-modal';
|
import CollectionSelectionModal from './collection-selection-modal';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
import { getProductCollectionUIStateInEditor } from '../utils';
|
||||||
|
import ProductPicker from './ProductPicker';
|
||||||
|
|
||||||
const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
const { clientId, attributes } = props;
|
const { clientId, attributes } = props;
|
||||||
|
const location = useGetLocation( props.context, props.clientId );
|
||||||
|
|
||||||
const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );
|
const [ isSelectionModalOpen, setIsSelectionModalOpen ] = useState( false );
|
||||||
const hasInnerBlocks = useSelect(
|
const hasInnerBlocks = useSelect(
|
||||||
|
@ -24,9 +31,37 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
[ clientId ]
|
[ clientId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const Component = hasInnerBlocks
|
const productCollectionUIStateInEditor =
|
||||||
? ProductCollectionContent
|
getProductCollectionUIStateInEditor( {
|
||||||
: ProductCollectionPlaceholder;
|
hasInnerBlocks,
|
||||||
|
location,
|
||||||
|
attributes: props.attributes,
|
||||||
|
usesReference: props.usesReference,
|
||||||
|
} );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render based on the UI state.
|
||||||
|
*/
|
||||||
|
let Component,
|
||||||
|
isUsingReferencePreviewMode = false;
|
||||||
|
switch ( productCollectionUIStateInEditor ) {
|
||||||
|
case ProductCollectionUIStatesInEditor.COLLECTION_PICKER:
|
||||||
|
Component = ProductCollectionPlaceholder;
|
||||||
|
break;
|
||||||
|
case ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER:
|
||||||
|
Component = ProductPicker;
|
||||||
|
break;
|
||||||
|
case ProductCollectionUIStatesInEditor.VALID:
|
||||||
|
Component = ProductCollectionContent;
|
||||||
|
break;
|
||||||
|
case ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW:
|
||||||
|
Component = ProductCollectionContent;
|
||||||
|
isUsingReferencePreviewMode = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// By default showing collection chooser.
|
||||||
|
Component = ProductCollectionPlaceholder;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -35,6 +70,9 @@ const Edit = ( props: ProductCollectionEditComponentProps ) => {
|
||||||
openCollectionSelectionModal={ () =>
|
openCollectionSelectionModal={ () =>
|
||||||
setIsSelectionModalOpen( true )
|
setIsSelectionModalOpen( true )
|
||||||
}
|
}
|
||||||
|
isUsingReferencePreviewMode={ isUsingReferencePreviewMode }
|
||||||
|
location={ location }
|
||||||
|
usesReference={ props.usesReference }
|
||||||
/>
|
/>
|
||||||
{ isSelectionModalOpen && (
|
{ isSelectionModalOpen && (
|
||||||
<CollectionSelectionModal
|
<CollectionSelectionModal
|
||||||
|
|
|
@ -50,6 +50,7 @@ import LayoutOptionsControl from './layout-options-control';
|
||||||
import FeaturedProductsControl from './featured-products-control';
|
import FeaturedProductsControl from './featured-products-control';
|
||||||
import CreatedControl from './created-control';
|
import CreatedControl from './created-control';
|
||||||
import PriceRangeControl from './price-range-control';
|
import PriceRangeControl from './price-range-control';
|
||||||
|
import LinkedProductControl from './linked-product-control';
|
||||||
|
|
||||||
const prepareShouldShowFilter =
|
const prepareShouldShowFilter =
|
||||||
( hideControls: FilterName[] ) => ( filter: FilterName ) => {
|
( hideControls: FilterName[] ) => ( filter: FilterName ) => {
|
||||||
|
@ -121,6 +122,13 @@ const ProductCollectionInspectorControls = (
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InspectorControls>
|
<InspectorControls>
|
||||||
|
<LinkedProductControl
|
||||||
|
query={ props.attributes.query }
|
||||||
|
setAttributes={ props.setAttributes }
|
||||||
|
usesReference={ props.usesReference }
|
||||||
|
location={ props.location }
|
||||||
|
/>
|
||||||
|
|
||||||
<ToolsPanel
|
<ToolsPanel
|
||||||
label={ __( 'Settings', 'woocommerce' ) }
|
label={ __( 'Settings', 'woocommerce' ) }
|
||||||
resetAll={ () => {
|
resetAll={ () => {
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import ProductControl from '@woocommerce/editor-components/product-control';
|
||||||
|
import { SelectedOption } from '@woocommerce/block-hocs';
|
||||||
|
import { useState, useMemo } from '@wordpress/element';
|
||||||
|
import type { WooCommerceBlockLocation } from '@woocommerce/blocks/product-template/utils';
|
||||||
|
import type { ProductResponseItem } from '@woocommerce/types';
|
||||||
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
import {
|
||||||
|
PanelBody,
|
||||||
|
PanelRow,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
FlexItem,
|
||||||
|
Dropdown,
|
||||||
|
// @ts-expect-error Using experimental features
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalText as Text,
|
||||||
|
Spinner,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { useGetProduct } from '../../utils';
|
||||||
|
import type {
|
||||||
|
ProductCollectionQuery,
|
||||||
|
ProductCollectionSetAttributes,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
const ProductButton: React.FC< {
|
||||||
|
isOpen: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
product: ProductResponseItem | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
} > = ( { isOpen, onToggle, product, isLoading } ) => {
|
||||||
|
if ( isLoading && ! product ) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="wc-block-product-collection-linked-product-control__button"
|
||||||
|
onClick={ onToggle }
|
||||||
|
aria-expanded={ isOpen }
|
||||||
|
disabled={ isLoading }
|
||||||
|
>
|
||||||
|
<Flex direction="row" expanded justify="flex-start">
|
||||||
|
<FlexItem className="wc-block-product-collection-linked-product-control__image-container">
|
||||||
|
<img
|
||||||
|
src={ product?.images?.[ 0 ]?.src }
|
||||||
|
alt={ product?.name }
|
||||||
|
/>
|
||||||
|
</FlexItem>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
align="flex-start"
|
||||||
|
gap={ 1 }
|
||||||
|
className="wc-block-product-collection-linked-product-control__content"
|
||||||
|
>
|
||||||
|
<FlexItem>
|
||||||
|
<Text color="inherit" lineHeight={ 1 }>
|
||||||
|
{ product?.name
|
||||||
|
? decodeEntities( product.name )
|
||||||
|
: '' }
|
||||||
|
</Text>
|
||||||
|
</FlexItem>
|
||||||
|
<FlexItem>
|
||||||
|
<Text color="inherit" lineHeight={ 1 }>
|
||||||
|
{ product?.sku }
|
||||||
|
</Text>
|
||||||
|
</FlexItem>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinkedProductPopoverContent: React.FC< {
|
||||||
|
query: ProductCollectionQuery;
|
||||||
|
setAttributes: ProductCollectionSetAttributes;
|
||||||
|
setIsDropdownOpen: React.Dispatch< React.SetStateAction< boolean > >;
|
||||||
|
} > = ( { query, setAttributes, setIsDropdownOpen } ) => (
|
||||||
|
<ProductControl
|
||||||
|
selected={ query?.productReference as SelectedOption }
|
||||||
|
onChange={ ( value: SelectedOption[] = [] ) => {
|
||||||
|
const productId = value[ 0 ]?.id ?? null;
|
||||||
|
if ( productId !== null ) {
|
||||||
|
setAttributes( {
|
||||||
|
query: {
|
||||||
|
...query,
|
||||||
|
productReference: productId,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
setIsDropdownOpen( false );
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
messages={ {
|
||||||
|
search: __( 'Select a product', 'woocommerce' ),
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinkedProductControl = ( {
|
||||||
|
query,
|
||||||
|
setAttributes,
|
||||||
|
location,
|
||||||
|
usesReference,
|
||||||
|
}: {
|
||||||
|
query: ProductCollectionQuery;
|
||||||
|
setAttributes: ProductCollectionSetAttributes;
|
||||||
|
location: WooCommerceBlockLocation;
|
||||||
|
usesReference: string[] | undefined;
|
||||||
|
} ) => {
|
||||||
|
const [ isDropdownOpen, setIsDropdownOpen ] = useState< boolean >( false );
|
||||||
|
const { product, isLoading } = useGetProduct( query.productReference );
|
||||||
|
|
||||||
|
const showLinkedProductControl = useMemo( () => {
|
||||||
|
const isInRequiredLocation = usesReference?.includes( location.type );
|
||||||
|
const isProductContextRequired = usesReference?.includes( 'product' );
|
||||||
|
const isProductContextSelected =
|
||||||
|
( query?.productReference ?? null ) !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
isProductContextRequired &&
|
||||||
|
! isInRequiredLocation &&
|
||||||
|
isProductContextSelected
|
||||||
|
);
|
||||||
|
}, [ location.type, query?.productReference, usesReference ] );
|
||||||
|
|
||||||
|
if ( ! showLinkedProductControl ) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelBody title={ __( 'Linked Product', 'woocommerce' ) }>
|
||||||
|
<PanelRow>
|
||||||
|
<Dropdown
|
||||||
|
className="wc-block-product-collection-linked-product-control"
|
||||||
|
contentClassName="wc-block-product-collection-linked-product__popover-content"
|
||||||
|
popoverProps={ { placement: 'left-start' } }
|
||||||
|
renderToggle={ ( { isOpen, onToggle } ) => (
|
||||||
|
<ProductButton
|
||||||
|
isOpen={ isOpen }
|
||||||
|
onToggle={ onToggle }
|
||||||
|
product={ product }
|
||||||
|
isLoading={ isLoading }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
renderContent={ () => (
|
||||||
|
<LinkedProductPopoverContent
|
||||||
|
query={ query }
|
||||||
|
setAttributes={ setAttributes }
|
||||||
|
setIsDropdownOpen={ setIsDropdownOpen }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
open={ isDropdownOpen }
|
||||||
|
onToggle={ () => setIsDropdownOpen( ! isDropdownOpen ) }
|
||||||
|
/>
|
||||||
|
</PanelRow>
|
||||||
|
</PanelBody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkedProductControl;
|
|
@ -10,7 +10,6 @@ import { useInstanceId } from '@wordpress/compose';
|
||||||
import { useEffect, useRef, useMemo } from '@wordpress/element';
|
import { useEffect, useRef, useMemo } from '@wordpress/element';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
import { useGetLocation } from '@woocommerce/blocks/product-template/utils';
|
|
||||||
import fastDeepEqual from 'fast-deep-equal/es6';
|
import fastDeepEqual from 'fast-deep-equal/es6';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,19 +67,23 @@ const useQueryId = (
|
||||||
|
|
||||||
const ProductCollectionContent = ( {
|
const ProductCollectionContent = ( {
|
||||||
preview: { setPreviewState, initialPreviewState } = {},
|
preview: { setPreviewState, initialPreviewState } = {},
|
||||||
usesReference,
|
|
||||||
...props
|
...props
|
||||||
}: ProductCollectionEditComponentProps ) => {
|
}: ProductCollectionEditComponentProps ) => {
|
||||||
const isInitialAttributesSet = useRef( false );
|
const isInitialAttributesSet = useRef( false );
|
||||||
const { clientId, attributes, setAttributes } = props;
|
const {
|
||||||
const location = useGetLocation( props.context, props.clientId );
|
clientId,
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
location,
|
||||||
|
isUsingReferencePreviewMode,
|
||||||
|
} = props;
|
||||||
|
|
||||||
useSetPreviewState( {
|
useSetPreviewState( {
|
||||||
setPreviewState,
|
setPreviewState,
|
||||||
setAttributes,
|
setAttributes,
|
||||||
location,
|
location,
|
||||||
attributes,
|
attributes,
|
||||||
usesReference,
|
isUsingReferencePreviewMode,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const blockProps = useBlockProps();
|
const blockProps = useBlockProps();
|
||||||
|
|
|
@ -9,6 +9,16 @@ import { type AttributeMetadata } from '@woocommerce/types';
|
||||||
*/
|
*/
|
||||||
import { WooCommerceBlockLocation } from '../product-template/utils';
|
import { WooCommerceBlockLocation } from '../product-template/utils';
|
||||||
|
|
||||||
|
export enum ProductCollectionUIStatesInEditor {
|
||||||
|
COLLECTION_PICKER = 'collection_chooser',
|
||||||
|
PRODUCT_REFERENCE_PICKER = 'product_context_picker',
|
||||||
|
VALID_WITH_PREVIEW = 'uses_reference_preview_mode',
|
||||||
|
VALID = 'valid',
|
||||||
|
// Future states
|
||||||
|
// INVALID = 'invalid',
|
||||||
|
// DELETED_PRODUCT_REFERENCE = 'deleted_product_reference',
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProductCollectionAttributes {
|
export interface ProductCollectionAttributes {
|
||||||
query: ProductCollectionQuery;
|
query: ProductCollectionQuery;
|
||||||
queryId: number;
|
queryId: number;
|
||||||
|
@ -95,6 +105,7 @@ export interface ProductCollectionQuery {
|
||||||
woocommerceHandPickedProducts: string[];
|
woocommerceHandPickedProducts: string[];
|
||||||
priceRange: undefined | PriceRange;
|
priceRange: undefined | PriceRange;
|
||||||
filterable: boolean;
|
filterable: boolean;
|
||||||
|
productReference?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProductCollectionEditComponentProps =
|
export type ProductCollectionEditComponentProps =
|
||||||
|
@ -108,6 +119,8 @@ export type ProductCollectionEditComponentProps =
|
||||||
context: {
|
context: {
|
||||||
templateSlug: string;
|
templateSlug: string;
|
||||||
};
|
};
|
||||||
|
isUsingReferencePreviewMode: boolean;
|
||||||
|
location: WooCommerceBlockLocation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProductCollectionOrder = 'asc' | 'desc';
|
export type TProductCollectionOrder = 'asc' | 'desc';
|
||||||
|
|
|
@ -6,8 +6,10 @@ import { addFilter } from '@wordpress/hooks';
|
||||||
import { select } from '@wordpress/data';
|
import { select } from '@wordpress/data';
|
||||||
import { isWpVersion } from '@woocommerce/settings';
|
import { isWpVersion } from '@woocommerce/settings';
|
||||||
import type { BlockEditProps, Block } from '@wordpress/blocks';
|
import type { BlockEditProps, Block } from '@wordpress/blocks';
|
||||||
import { useLayoutEffect } from '@wordpress/element';
|
import { useEffect, useLayoutEffect, useState } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import type { ProductResponseItem } from '@woocommerce/types';
|
||||||
|
import { getProduct } from '@woocommerce/editor-components/utils';
|
||||||
import {
|
import {
|
||||||
createBlock,
|
createBlock,
|
||||||
// @ts-expect-error Type definitions for this function are missing in Guteberg
|
// @ts-expect-error Type definitions for this function are missing in Guteberg
|
||||||
|
@ -18,13 +20,14 @@ import {
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
type ProductCollectionAttributes,
|
ProductCollectionAttributes,
|
||||||
type TProductCollectionOrder,
|
TProductCollectionOrder,
|
||||||
type TProductCollectionOrderBy,
|
TProductCollectionOrderBy,
|
||||||
type ProductCollectionQuery,
|
ProductCollectionQuery,
|
||||||
type ProductCollectionDisplayLayout,
|
ProductCollectionDisplayLayout,
|
||||||
type PreviewState,
|
PreviewState,
|
||||||
type SetPreviewState,
|
SetPreviewState,
|
||||||
|
ProductCollectionUIStatesInEditor,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
coreQueryPaginationBlockName,
|
coreQueryPaginationBlockName,
|
||||||
|
@ -166,41 +169,14 @@ export const addProductCollectionToQueryPaginationParentOrAncestor = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the preview message for the Product Collection block based on the usesReference.
|
* Get the message to show in the preview label when the block is in preview mode based
|
||||||
* There are two scenarios:
|
* on the `usesReference` value.
|
||||||
* 1. When usesReference is product, the preview message will be:
|
|
||||||
* "Actual products will vary depending on the product being viewed."
|
|
||||||
* 2. For all other usesReference, the preview message will be:
|
|
||||||
* "Actual products will vary depending on the page being viewed."
|
|
||||||
*
|
|
||||||
* This message will be shown when the usesReference isn't available on the Editor side, but is available on the Frontend.
|
|
||||||
*/
|
*/
|
||||||
export const getUsesReferencePreviewMessage = (
|
export const getUsesReferencePreviewMessage = (
|
||||||
location: WooCommerceBlockLocation,
|
location: WooCommerceBlockLocation,
|
||||||
usesReference?: string[]
|
isUsingReferencePreviewMode: boolean
|
||||||
) => {
|
) => {
|
||||||
if ( ! ( Array.isArray( usesReference ) && usesReference.length > 0 ) ) {
|
if ( isUsingReferencePreviewMode ) {
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( usesReference.includes( location.type ) ) {
|
|
||||||
/**
|
|
||||||
* Block shouldn't be in preview mode when:
|
|
||||||
* 1. Current location is archive and termId is available.
|
|
||||||
* 2. Current location is product and productId is available.
|
|
||||||
*
|
|
||||||
* Because in these cases, we have required context on the editor side.
|
|
||||||
*/
|
|
||||||
const isArchiveLocationWithTermId =
|
|
||||||
location.type === LocationType.Archive &&
|
|
||||||
( location.sourceData?.termId ?? null ) !== null;
|
|
||||||
const isProductLocationWithProductId =
|
|
||||||
location.type === LocationType.Product &&
|
|
||||||
( location.sourceData?.productId ?? null ) !== null;
|
|
||||||
if ( isArchiveLocationWithTermId || isProductLocationWithProductId ) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( location.type === LocationType.Product ) {
|
if ( location.type === LocationType.Product ) {
|
||||||
return __(
|
return __(
|
||||||
'Actual products will vary depending on the product being viewed.',
|
'Actual products will vary depending on the product being viewed.',
|
||||||
|
@ -217,12 +193,77 @@ export const getUsesReferencePreviewMessage = (
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getProductCollectionUIStateInEditor = ( {
|
||||||
|
location,
|
||||||
|
usesReference,
|
||||||
|
attributes,
|
||||||
|
hasInnerBlocks,
|
||||||
|
}: {
|
||||||
|
location: WooCommerceBlockLocation;
|
||||||
|
usesReference?: string[] | undefined;
|
||||||
|
attributes: ProductCollectionAttributes;
|
||||||
|
hasInnerBlocks: boolean;
|
||||||
|
} ): ProductCollectionUIStatesInEditor => {
|
||||||
|
const isInRequiredLocation = usesReference?.includes( location.type );
|
||||||
|
const isCollectionSelected = !! attributes.collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case 1: Product context picker
|
||||||
|
*/
|
||||||
|
const isProductContextRequired = usesReference?.includes( 'product' );
|
||||||
|
const isProductContextSelected =
|
||||||
|
( attributes.query?.productReference ?? null ) !== null;
|
||||||
|
if (
|
||||||
|
isCollectionSelected &&
|
||||||
|
isProductContextRequired &&
|
||||||
|
! isInRequiredLocation &&
|
||||||
|
! isProductContextSelected
|
||||||
|
) {
|
||||||
|
return ProductCollectionUIStatesInEditor.PRODUCT_REFERENCE_PICKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case 2: Preview mode - based on `usesReference` value
|
||||||
|
*/
|
||||||
|
if ( isInRequiredLocation ) {
|
||||||
|
/**
|
||||||
|
* Block shouldn't be in preview mode when:
|
||||||
|
* 1. Current location is archive and termId is available.
|
||||||
|
* 2. Current location is product and productId is available.
|
||||||
|
*
|
||||||
|
* Because in these cases, we have required context on the editor side.
|
||||||
|
*/
|
||||||
|
const isArchiveLocationWithTermId =
|
||||||
|
location.type === LocationType.Archive &&
|
||||||
|
( location.sourceData?.termId ?? null ) !== null;
|
||||||
|
const isProductLocationWithProductId =
|
||||||
|
location.type === LocationType.Product &&
|
||||||
|
( location.sourceData?.productId ?? null ) !== null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
! isArchiveLocationWithTermId &&
|
||||||
|
! isProductLocationWithProductId
|
||||||
|
) {
|
||||||
|
return ProductCollectionUIStatesInEditor.VALID_WITH_PREVIEW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case 3: Collection chooser
|
||||||
|
*/
|
||||||
|
if ( ! hasInnerBlocks && ! isCollectionSelected ) {
|
||||||
|
return ProductCollectionUIStatesInEditor.COLLECTION_PICKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProductCollectionUIStatesInEditor.VALID;
|
||||||
|
};
|
||||||
|
|
||||||
export const useSetPreviewState = ( {
|
export const useSetPreviewState = ( {
|
||||||
setPreviewState,
|
setPreviewState,
|
||||||
location,
|
location,
|
||||||
attributes,
|
attributes,
|
||||||
setAttributes,
|
setAttributes,
|
||||||
usesReference,
|
isUsingReferencePreviewMode,
|
||||||
}: {
|
}: {
|
||||||
setPreviewState?: SetPreviewState | undefined;
|
setPreviewState?: SetPreviewState | undefined;
|
||||||
location: WooCommerceBlockLocation;
|
location: WooCommerceBlockLocation;
|
||||||
|
@ -231,6 +272,7 @@ export const useSetPreviewState = ( {
|
||||||
attributes: Partial< ProductCollectionAttributes >
|
attributes: Partial< ProductCollectionAttributes >
|
||||||
) => void;
|
) => void;
|
||||||
usesReference?: string[] | undefined;
|
usesReference?: string[] | undefined;
|
||||||
|
isUsingReferencePreviewMode: boolean;
|
||||||
} ) => {
|
} ) => {
|
||||||
const setState = ( newPreviewState: PreviewState ) => {
|
const setState = ( newPreviewState: PreviewState ) => {
|
||||||
setAttributes( {
|
setAttributes( {
|
||||||
|
@ -240,8 +282,6 @@ export const useSetPreviewState = ( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
const isCollectionUsesReference =
|
|
||||||
usesReference && usesReference?.length > 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When usesReference is available on Frontend but not on Editor side,
|
* When usesReference is available on Frontend but not on Editor side,
|
||||||
|
@ -249,10 +289,10 @@ export const useSetPreviewState = ( {
|
||||||
*/
|
*/
|
||||||
const usesReferencePreviewMessage = getUsesReferencePreviewMessage(
|
const usesReferencePreviewMessage = getUsesReferencePreviewMessage(
|
||||||
location,
|
location,
|
||||||
usesReference
|
isUsingReferencePreviewMode
|
||||||
);
|
);
|
||||||
useLayoutEffect( () => {
|
useLayoutEffect( () => {
|
||||||
if ( isCollectionUsesReference ) {
|
if ( isUsingReferencePreviewMode ) {
|
||||||
setAttributes( {
|
setAttributes( {
|
||||||
__privatePreviewState: {
|
__privatePreviewState: {
|
||||||
isPreview: usesReferencePreviewMessage.length > 0,
|
isPreview: usesReferencePreviewMessage.length > 0,
|
||||||
|
@ -263,12 +303,12 @@ export const useSetPreviewState = ( {
|
||||||
}, [
|
}, [
|
||||||
setAttributes,
|
setAttributes,
|
||||||
usesReferencePreviewMessage,
|
usesReferencePreviewMessage,
|
||||||
isCollectionUsesReference,
|
isUsingReferencePreviewMode,
|
||||||
] );
|
] );
|
||||||
|
|
||||||
// Running setPreviewState function provided by Collection, if it exists.
|
// Running setPreviewState function provided by Collection, if it exists.
|
||||||
useLayoutEffect( () => {
|
useLayoutEffect( () => {
|
||||||
if ( ! setPreviewState && ! isCollectionUsesReference ) {
|
if ( ! setPreviewState && ! isUsingReferencePreviewMode ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,11 +334,14 @@ export const useSetPreviewState = ( {
|
||||||
* - Products by tag
|
* - Products by tag
|
||||||
* - Products by attribute
|
* - Products by attribute
|
||||||
*/
|
*/
|
||||||
|
const termId =
|
||||||
|
location.type === LocationType.Archive
|
||||||
|
? location.sourceData?.termId
|
||||||
|
: null;
|
||||||
useLayoutEffect( () => {
|
useLayoutEffect( () => {
|
||||||
if ( ! setPreviewState && ! isCollectionUsesReference ) {
|
if ( ! setPreviewState && ! isUsingReferencePreviewMode ) {
|
||||||
const isGenericArchiveTemplate =
|
const isGenericArchiveTemplate =
|
||||||
location.type === LocationType.Archive &&
|
location.type === LocationType.Archive && termId === null;
|
||||||
location.sourceData?.termId === null;
|
|
||||||
|
|
||||||
setAttributes( {
|
setAttributes( {
|
||||||
__privatePreviewState: {
|
__privatePreviewState: {
|
||||||
|
@ -315,11 +358,11 @@ export const useSetPreviewState = ( {
|
||||||
}, [
|
}, [
|
||||||
attributes?.query?.inherit,
|
attributes?.query?.inherit,
|
||||||
usesReferencePreviewMessage,
|
usesReferencePreviewMessage,
|
||||||
location.sourceData?.termId,
|
termId,
|
||||||
location.type,
|
location.type,
|
||||||
setAttributes,
|
setAttributes,
|
||||||
setPreviewState,
|
setPreviewState,
|
||||||
isCollectionUsesReference,
|
isUsingReferencePreviewMode,
|
||||||
] );
|
] );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -356,3 +399,35 @@ export const getDefaultProductCollection = () =>
|
||||||
},
|
},
|
||||||
createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )
|
createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const useGetProduct = ( productId: number | undefined ) => {
|
||||||
|
const [ product, setProduct ] = useState< ProductResponseItem | null >(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [ isLoading, setIsLoading ] = useState< boolean >( false );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
const fetchProduct = async () => {
|
||||||
|
if ( productId ) {
|
||||||
|
setIsLoading( true );
|
||||||
|
try {
|
||||||
|
const fetchedProduct = ( await getProduct(
|
||||||
|
productId
|
||||||
|
) ) as ProductResponseItem;
|
||||||
|
setProduct( fetchedProduct );
|
||||||
|
} catch ( error ) {
|
||||||
|
setProduct( null );
|
||||||
|
} finally {
|
||||||
|
setIsLoading( false );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setProduct( null );
|
||||||
|
setIsLoading( false );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchProduct();
|
||||||
|
}, [ productId ] );
|
||||||
|
|
||||||
|
return { product, isLoading };
|
||||||
|
};
|
||||||
|
|
|
@ -266,7 +266,7 @@ const ProductTemplateEdit = (
|
||||||
products: getEntityRecords( 'postType', postType, {
|
products: getEntityRecords( 'postType', postType, {
|
||||||
...query,
|
...query,
|
||||||
...restQueryArgs,
|
...restQueryArgs,
|
||||||
location,
|
productCollectionLocation: location,
|
||||||
productCollectionQueryContext,
|
productCollectionQueryContext,
|
||||||
previewState: __privateProductCollectionPreviewState,
|
previewState: __privateProductCollectionPreviewState,
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -63,17 +63,65 @@ const prepareIsInGenericTemplate =
|
||||||
( entitySlug: string ): boolean =>
|
( entitySlug: string ): boolean =>
|
||||||
templateSlug === entitySlug;
|
templateSlug === entitySlug;
|
||||||
|
|
||||||
export type WooCommerceBlockLocation = ReturnType<
|
interface WooCommerceBaseLocation {
|
||||||
typeof createLocationObject
|
type: LocationType;
|
||||||
>;
|
sourceData?: object | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const createLocationObject = (
|
interface ProductLocation extends WooCommerceBaseLocation {
|
||||||
type: LocationType,
|
type: LocationType.Product;
|
||||||
sourceData: Record< string, unknown > = {}
|
sourceData?:
|
||||||
) => ( {
|
| {
|
||||||
type,
|
productId: number;
|
||||||
sourceData,
|
}
|
||||||
} );
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArchiveLocation extends WooCommerceBaseLocation {
|
||||||
|
type: LocationType.Archive;
|
||||||
|
sourceData?:
|
||||||
|
| {
|
||||||
|
taxonomy: string;
|
||||||
|
termId: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CartLocation extends WooCommerceBaseLocation {
|
||||||
|
type: LocationType.Cart;
|
||||||
|
sourceData?:
|
||||||
|
| {
|
||||||
|
productIds: number[];
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrderLocation extends WooCommerceBaseLocation {
|
||||||
|
type: LocationType.Order;
|
||||||
|
sourceData?:
|
||||||
|
| {
|
||||||
|
orderId: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SiteLocation extends WooCommerceBaseLocation {
|
||||||
|
type: LocationType.Site;
|
||||||
|
sourceData?: object | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WooCommerceBlockLocation =
|
||||||
|
| ProductLocation
|
||||||
|
| ArchiveLocation
|
||||||
|
| CartLocation
|
||||||
|
| OrderLocation
|
||||||
|
| SiteLocation;
|
||||||
|
|
||||||
|
const createLocationObject = ( type: LocationType, sourceData: object = {} ) =>
|
||||||
|
( {
|
||||||
|
type,
|
||||||
|
sourceData,
|
||||||
|
} as WooCommerceBlockLocation );
|
||||||
|
|
||||||
type ContextProperties = {
|
type ContextProperties = {
|
||||||
templateSlug: string;
|
templateSlug: string;
|
||||||
|
@ -83,7 +131,7 @@ type ContextProperties = {
|
||||||
export const useGetLocation = < T, >(
|
export const useGetLocation = < T, >(
|
||||||
context: Context< T & ContextProperties >,
|
context: Context< T & ContextProperties >,
|
||||||
clientId: string
|
clientId: string
|
||||||
) => {
|
): WooCommerceBlockLocation => {
|
||||||
const templateSlug = context.templateSlug || '';
|
const templateSlug = context.templateSlug || '';
|
||||||
const postId = context.postId || null;
|
const postId = context.postId || null;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
|
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
|
||||||
|
|
||||||
const Save = () => {
|
const Save = () => {
|
||||||
const blockProps = useBlockProps.save();
|
const blockProps = useBlockProps.save( {
|
||||||
|
className: 'woocommerce',
|
||||||
|
} );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
|
|
|
@ -62,6 +62,16 @@ interface ProductControlProps {
|
||||||
* Whether to show variations in the list of items available.
|
* Whether to show variations in the list of items available.
|
||||||
*/
|
*/
|
||||||
showVariations?: boolean;
|
showVariations?: boolean;
|
||||||
|
/**
|
||||||
|
* Different messages to display in the component.
|
||||||
|
* If any of the messages are not provided, the default message will be used.
|
||||||
|
*/
|
||||||
|
messages?: {
|
||||||
|
list?: string;
|
||||||
|
noItems?: string;
|
||||||
|
search?: string;
|
||||||
|
updated?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
|
@ -188,7 +198,7 @@ const ProductControl = (
|
||||||
} else if ( showVariations ) {
|
} else if ( showVariations ) {
|
||||||
return renderItemWithVariations;
|
return renderItemWithVariations;
|
||||||
}
|
}
|
||||||
return () => null;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( error ) {
|
if ( error ) {
|
||||||
|
@ -216,7 +226,10 @@ const ProductControl = (
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
renderItem={ getRenderItemFunc() }
|
renderItem={ getRenderItemFunc() }
|
||||||
onSearch={ onSearch }
|
onSearch={ onSearch }
|
||||||
messages={ messages }
|
messages={ {
|
||||||
|
...messages,
|
||||||
|
...props.messages,
|
||||||
|
} }
|
||||||
isHierarchical
|
isHierarchical
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { test as base, expect } from '@woocommerce/e2e-utils';
|
||||||
*/
|
*/
|
||||||
import ProductCollectionPage, {
|
import ProductCollectionPage, {
|
||||||
BLOCK_LABELS,
|
BLOCK_LABELS,
|
||||||
|
Collections,
|
||||||
SELECTORS,
|
SELECTORS,
|
||||||
} from './product-collection.page';
|
} from './product-collection.page';
|
||||||
|
|
||||||
|
@ -81,6 +82,87 @@ test.describe( 'Product Collection', () => {
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
test.describe( 'when no results are found', () => {
|
||||||
|
test.beforeEach( async ( { admin } ) => {
|
||||||
|
await admin.createNewPost();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'does not render', async ( { page, editor, pageObject } ) => {
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( 'featured' );
|
||||||
|
await pageObject.addFilter( 'Price Range' );
|
||||||
|
await pageObject.setPriceRange( {
|
||||||
|
max: '1',
|
||||||
|
} );
|
||||||
|
|
||||||
|
const featuredBlock = editor.canvas.getByLabel( 'Block: Featured' );
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
featuredBlock.getByText( 'Featured products' )
|
||||||
|
).toBeVisible();
|
||||||
|
// The "No results found" info is rendered in editor for all collections.
|
||||||
|
await expect(
|
||||||
|
featuredBlock.getByText( 'No results found' )
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await pageObject.publishAndGoToFrontend();
|
||||||
|
|
||||||
|
const content = page.locator( 'main' );
|
||||||
|
|
||||||
|
await expect( content ).not.toContainText( 'Featured products' );
|
||||||
|
await expect( content ).not.toContainText( 'No results found' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// This test ensures the runtime render state is correctly reset for
|
||||||
|
// each block.
|
||||||
|
test( 'does not prevent subsequent blocks from render', async ( {
|
||||||
|
page,
|
||||||
|
pageObject,
|
||||||
|
} ) => {
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( 'featured' );
|
||||||
|
await pageObject.addFilter( 'Price Range' );
|
||||||
|
await pageObject.setPriceRange( {
|
||||||
|
max: '1',
|
||||||
|
} );
|
||||||
|
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( 'topRated' );
|
||||||
|
|
||||||
|
await pageObject.refreshLocators( 'editor' );
|
||||||
|
await expect( pageObject.products ).toHaveCount( 5 );
|
||||||
|
|
||||||
|
await pageObject.publishAndGoToFrontend();
|
||||||
|
|
||||||
|
await pageObject.refreshLocators( 'frontend' );
|
||||||
|
await expect( pageObject.products ).toHaveCount( 5 );
|
||||||
|
await expect( page.locator( 'main' ) ).not.toContainText(
|
||||||
|
'Featured products'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'renders if No Results block is present', async ( {
|
||||||
|
page,
|
||||||
|
editor,
|
||||||
|
pageObject,
|
||||||
|
} ) => {
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( 'productCatalog' );
|
||||||
|
await pageObject.addFilter( 'Price Range' );
|
||||||
|
await pageObject.setPriceRange( {
|
||||||
|
max: '1',
|
||||||
|
} );
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
editor.canvas.getByText( 'No results found' )
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await pageObject.publishAndGoToFrontend();
|
||||||
|
|
||||||
|
await expect( page.getByText( 'No results found' ) ).toBeVisible();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
test.describe( 'Renders correctly with all Product Elements', () => {
|
test.describe( 'Renders correctly with all Product Elements', () => {
|
||||||
const expectedProductContent = [
|
const expectedProductContent = [
|
||||||
'Beanie', // core/post-title
|
'Beanie', // core/post-title
|
||||||
|
@ -321,7 +403,7 @@ test.describe( 'Product Collection', () => {
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.describe( 'Location is recognised', () => {
|
test.describe( 'Location is recognized', () => {
|
||||||
const filterRequest = ( request: Request ) => {
|
const filterRequest = ( request: Request ) => {
|
||||||
const url = request.url();
|
const url = request.url();
|
||||||
return (
|
return (
|
||||||
|
@ -337,7 +419,9 @@ test.describe( 'Product Collection', () => {
|
||||||
return (
|
return (
|
||||||
url.includes( 'wp/v2/product' ) &&
|
url.includes( 'wp/v2/product' ) &&
|
||||||
searchParams.get( 'isProductCollectionBlock' ) === 'true' &&
|
searchParams.get( 'isProductCollectionBlock' ) === 'true' &&
|
||||||
!! searchParams.get( `location[sourceData][productId]` )
|
!! searchParams.get(
|
||||||
|
`productCollectionLocation[sourceData][productId]`
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -349,26 +433,30 @@ test.describe( 'Product Collection', () => {
|
||||||
|
|
||||||
if ( locationType === 'product' ) {
|
if ( locationType === 'product' ) {
|
||||||
return {
|
return {
|
||||||
type: searchParams.get( 'location[type]' ),
|
type: searchParams.get( 'productCollectionLocation[type]' ),
|
||||||
productId: searchParams.get(
|
productId: searchParams.get(
|
||||||
`location[sourceData][productId]`
|
`productCollectionLocation[sourceData][productId]`
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( locationType === 'archive' ) {
|
if ( locationType === 'archive' ) {
|
||||||
return {
|
return {
|
||||||
type: searchParams.get( 'location[type]' ),
|
type: searchParams.get( 'productCollectionLocation[type]' ),
|
||||||
taxonomy: searchParams.get(
|
taxonomy: searchParams.get(
|
||||||
`location[sourceData][taxonomy]`
|
`productCollectionLocation[sourceData][taxonomy]`
|
||||||
|
),
|
||||||
|
termId: searchParams.get(
|
||||||
|
`productCollectionLocation[sourceData][termId]`
|
||||||
),
|
),
|
||||||
termId: searchParams.get( `location[sourceData][termId]` ),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: searchParams.get( 'location[type]' ),
|
type: searchParams.get( 'productCollectionLocation[type]' ),
|
||||||
sourceData: searchParams.get( `location[sourceData]` ),
|
sourceData: searchParams.get(
|
||||||
|
`productCollectionLocation[sourceData]`
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -401,10 +489,10 @@ test.describe( 'Product Collection', () => {
|
||||||
pageObject.BLOCK_NAME
|
pageObject.BLOCK_NAME
|
||||||
);
|
);
|
||||||
|
|
||||||
const locationReuqestPromise =
|
const locationRequestPromise =
|
||||||
page.waitForRequest( filterProductRequest );
|
page.waitForRequest( filterProductRequest );
|
||||||
await pageObject.chooseCollectionInTemplate( 'featured' );
|
await pageObject.chooseCollectionInTemplate( 'featured' );
|
||||||
const locationRequest = await locationReuqestPromise;
|
const locationRequest = await locationRequestPromise;
|
||||||
|
|
||||||
const { type, productId } = getLocationDetailsFromRequest(
|
const { type, productId } = getLocationDetailsFromRequest(
|
||||||
locationRequest,
|
locationRequest,
|
||||||
|
@ -880,3 +968,309 @@ test.describe( 'Product Collection', () => {
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
test.describe( 'Testing "usesReference" argument in "registerProductCollection"', () => {
|
||||||
|
const MY_REGISTERED_COLLECTIONS = {
|
||||||
|
myCustomCollectionWithProductContext: {
|
||||||
|
name: 'My Custom Collection - Product Context',
|
||||||
|
label: 'Block: My Custom Collection - Product Context',
|
||||||
|
previewLabelTemplate: [ 'woocommerce/woocommerce//single-product' ],
|
||||||
|
shouldShowProductPicker: true,
|
||||||
|
},
|
||||||
|
myCustomCollectionWithCartContext: {
|
||||||
|
name: 'My Custom Collection - Cart Context',
|
||||||
|
label: 'Block: My Custom Collection - Cart Context',
|
||||||
|
previewLabelTemplate: [ 'woocommerce/woocommerce//page-cart' ],
|
||||||
|
shouldShowProductPicker: false,
|
||||||
|
},
|
||||||
|
myCustomCollectionWithOrderContext: {
|
||||||
|
name: 'My Custom Collection - Order Context',
|
||||||
|
label: 'Block: My Custom Collection - Order Context',
|
||||||
|
previewLabelTemplate: [
|
||||||
|
'woocommerce/woocommerce//order-confirmation',
|
||||||
|
],
|
||||||
|
shouldShowProductPicker: false,
|
||||||
|
},
|
||||||
|
myCustomCollectionWithArchiveContext: {
|
||||||
|
name: 'My Custom Collection - Archive Context',
|
||||||
|
label: 'Block: My Custom Collection - Archive Context',
|
||||||
|
previewLabelTemplate: [
|
||||||
|
'woocommerce/woocommerce//taxonomy-product_cat',
|
||||||
|
],
|
||||||
|
shouldShowProductPicker: false,
|
||||||
|
},
|
||||||
|
myCustomCollectionMultipleContexts: {
|
||||||
|
name: 'My Custom Collection - Multiple Contexts',
|
||||||
|
label: 'Block: My Custom Collection - Multiple Contexts',
|
||||||
|
previewLabelTemplate: [
|
||||||
|
'woocommerce/woocommerce//single-product',
|
||||||
|
'woocommerce/woocommerce//order-confirmation',
|
||||||
|
],
|
||||||
|
shouldShowProductPicker: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Activate plugin which registers custom product collections
|
||||||
|
test.beforeEach( async ( { requestUtils } ) => {
|
||||||
|
await requestUtils.activatePlugin(
|
||||||
|
'register-product-collection-tester'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
Object.entries( MY_REGISTERED_COLLECTIONS ).forEach(
|
||||||
|
( [ key, collection ] ) => {
|
||||||
|
for ( const template of collection.previewLabelTemplate ) {
|
||||||
|
test( `Collection "${ collection.name }" should show preview label in "${ template }"`, async ( {
|
||||||
|
pageObject,
|
||||||
|
editor,
|
||||||
|
} ) => {
|
||||||
|
await pageObject.goToEditorTemplate( template );
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInTemplate(
|
||||||
|
key as Collections
|
||||||
|
);
|
||||||
|
|
||||||
|
const block = editor.canvas.getByLabel( collection.label );
|
||||||
|
const previewButtonLocator = block.getByTestId(
|
||||||
|
SELECTORS.previewButtonTestID
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( previewButtonLocator ).toBeVisible();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
test( `Collection "${ collection.name }" should not show preview label in a post`, async ( {
|
||||||
|
pageObject,
|
||||||
|
editor,
|
||||||
|
admin,
|
||||||
|
} ) => {
|
||||||
|
await admin.createNewPost();
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( key as Collections );
|
||||||
|
|
||||||
|
// Check visibility of product picker
|
||||||
|
const editorProductPicker = editor.canvas.locator(
|
||||||
|
SELECTORS.productPicker
|
||||||
|
);
|
||||||
|
const expectedVisibility = collection.shouldShowProductPicker
|
||||||
|
? 'toBeVisible'
|
||||||
|
: 'toBeHidden';
|
||||||
|
await expect( editorProductPicker )[ expectedVisibility ]();
|
||||||
|
|
||||||
|
if ( collection.shouldShowProductPicker ) {
|
||||||
|
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
editor.canvas
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, the product picker should be hidden
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
|
||||||
|
// Check visibility of preview label
|
||||||
|
const block = editor.canvas.getByLabel( collection.label );
|
||||||
|
const previewButtonLocator = block.getByTestId(
|
||||||
|
SELECTORS.previewButtonTestID
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( previewButtonLocator ).toBeHidden();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( `Collection "${ collection.name }" should not show preview label in Product Catalog template`, async ( {
|
||||||
|
pageObject,
|
||||||
|
editor,
|
||||||
|
} ) => {
|
||||||
|
await pageObject.goToProductCatalogAndInsertCollection(
|
||||||
|
key as Collections
|
||||||
|
);
|
||||||
|
|
||||||
|
const block = editor.canvas.getByLabel( collection.label );
|
||||||
|
const previewButtonLocator = block.getByTestId(
|
||||||
|
SELECTORS.previewButtonTestID
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( previewButtonLocator ).toBeHidden();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
test.describe( 'Product picker', () => {
|
||||||
|
const MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT = {
|
||||||
|
myCustomCollectionWithProductContext: {
|
||||||
|
name: 'My Custom Collection - Product Context',
|
||||||
|
label: 'Block: My Custom Collection - Product Context',
|
||||||
|
collection:
|
||||||
|
'woocommerce/product-collection/my-custom-collection-product-context',
|
||||||
|
},
|
||||||
|
myCustomCollectionMultipleContexts: {
|
||||||
|
name: 'My Custom Collection - Multiple Contexts',
|
||||||
|
label: 'Block: My Custom Collection - Multiple Contexts',
|
||||||
|
collection:
|
||||||
|
'woocommerce/product-collection/my-custom-collection-multiple-contexts',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Activate plugin which registers custom product collections
|
||||||
|
test.beforeEach( async ( { requestUtils } ) => {
|
||||||
|
await requestUtils.activatePlugin(
|
||||||
|
'register-product-collection-tester'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
Object.entries( MY_REGISTERED_COLLECTIONS_THAT_NEEDS_PRODUCT ).forEach(
|
||||||
|
( [ key, collection ] ) => {
|
||||||
|
test( `For collection "${ collection.name }" - manually selected product reference should be available on Frontend in a post`, async ( {
|
||||||
|
pageObject,
|
||||||
|
admin,
|
||||||
|
page,
|
||||||
|
editor,
|
||||||
|
} ) => {
|
||||||
|
await admin.createNewPost();
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( key as Collections );
|
||||||
|
|
||||||
|
// Verify that product picker is shown in Editor
|
||||||
|
const editorProductPicker = editor.canvas.locator(
|
||||||
|
SELECTORS.productPicker
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeVisible();
|
||||||
|
|
||||||
|
// Once a product is selected, the product picker should be hidden
|
||||||
|
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
editor.canvas
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
|
||||||
|
// On Frontend, verify that product reference is a number
|
||||||
|
await pageObject.publishAndGoToFrontend();
|
||||||
|
const collectionWithProductContext = page.locator(
|
||||||
|
`[data-collection="${ collection.collection }"]`
|
||||||
|
);
|
||||||
|
const queryAttribute = JSON.parse(
|
||||||
|
( await collectionWithProductContext.getAttribute(
|
||||||
|
'data-query'
|
||||||
|
) ) || '{}'
|
||||||
|
);
|
||||||
|
expect( typeof queryAttribute?.productReference ).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( `For collection "${ collection.name }" - changing product using inspector control`, async ( {
|
||||||
|
pageObject,
|
||||||
|
admin,
|
||||||
|
page,
|
||||||
|
editor,
|
||||||
|
} ) => {
|
||||||
|
await admin.createNewPost();
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost( key as Collections );
|
||||||
|
|
||||||
|
// Verify that product picker is shown in Editor
|
||||||
|
const editorProductPicker = editor.canvas.locator(
|
||||||
|
SELECTORS.productPicker
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeVisible();
|
||||||
|
|
||||||
|
// Once a product is selected, the product picker should be hidden
|
||||||
|
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
editor.canvas
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
|
||||||
|
// Verify that Album is selected
|
||||||
|
await expect(
|
||||||
|
admin.page.locator( SELECTORS.linkedProductControl.button )
|
||||||
|
).toContainText( 'Album' );
|
||||||
|
|
||||||
|
// Change product using inspector control to Beanie
|
||||||
|
await admin.page
|
||||||
|
.locator( SELECTORS.linkedProductControl.button )
|
||||||
|
.click();
|
||||||
|
await admin.page
|
||||||
|
.locator( SELECTORS.linkedProductControl.popoverContent )
|
||||||
|
.getByLabel( 'Beanie', { exact: true } )
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
admin.page.locator( SELECTORS.linkedProductControl.button )
|
||||||
|
).toContainText( 'Beanie' );
|
||||||
|
|
||||||
|
// On Frontend, verify that product reference is a number
|
||||||
|
await pageObject.publishAndGoToFrontend();
|
||||||
|
const collectionWithProductContext = page.locator(
|
||||||
|
`[data-collection="${ collection.collection }"]`
|
||||||
|
);
|
||||||
|
const queryAttribute = JSON.parse(
|
||||||
|
( await collectionWithProductContext.getAttribute(
|
||||||
|
'data-query'
|
||||||
|
) ) || '{}'
|
||||||
|
);
|
||||||
|
expect( typeof queryAttribute?.productReference ).toBe(
|
||||||
|
'number'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( `For collection "${ collection.name }" - product picker shouldn't be shown in Single Product template`, async ( {
|
||||||
|
pageObject,
|
||||||
|
admin,
|
||||||
|
editor,
|
||||||
|
} ) => {
|
||||||
|
await admin.visitSiteEditor( {
|
||||||
|
postId: `woocommerce/woocommerce//single-product`,
|
||||||
|
postType: 'wp_template',
|
||||||
|
canvas: 'edit',
|
||||||
|
} );
|
||||||
|
await editor.canvas.locator( 'body' ).click();
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInTemplate(
|
||||||
|
key as Collections
|
||||||
|
);
|
||||||
|
|
||||||
|
const editorProductPicker = editor.canvas.locator(
|
||||||
|
SELECTORS.productPicker
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test( 'Product picker should work as expected while changing collection using "Choose collection" button from Toolbar', async ( {
|
||||||
|
pageObject,
|
||||||
|
admin,
|
||||||
|
editor,
|
||||||
|
} ) => {
|
||||||
|
await admin.createNewPost();
|
||||||
|
await pageObject.insertProductCollection();
|
||||||
|
await pageObject.chooseCollectionInPost(
|
||||||
|
'myCustomCollectionWithProductContext'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that product picker is shown in Editor
|
||||||
|
const editorProductPicker = editor.canvas.locator(
|
||||||
|
SELECTORS.productPicker
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeVisible();
|
||||||
|
|
||||||
|
// Once a product is selected, the product picker should be hidden
|
||||||
|
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
editor.canvas
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
|
||||||
|
// Change collection using Toolbar
|
||||||
|
await pageObject.changeCollectionUsingToolbar(
|
||||||
|
'myCustomCollectionMultipleContexts'
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeVisible();
|
||||||
|
|
||||||
|
// Once a product is selected, the product picker should be hidden
|
||||||
|
await pageObject.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
editor.canvas
|
||||||
|
);
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
|
||||||
|
// Product picker should be hidden for collections that don't need product
|
||||||
|
await pageObject.changeCollectionUsingToolbar( 'featured' );
|
||||||
|
await expect( editorProductPicker ).toBeHidden();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Locator, Page } from '@playwright/test';
|
import { FrameLocator, Locator, Page } from '@playwright/test';
|
||||||
import { Editor, Admin } from '@woocommerce/e2e-utils';
|
import { Editor, Admin } from '@woocommerce/e2e-utils';
|
||||||
import { BlockRepresentation } from '@wordpress/e2e-test-utils-playwright/build-types/editor/insert-block';
|
import { BlockRepresentation } from '@wordpress/e2e-test-utils-playwright/build-types/editor/insert-block';
|
||||||
|
|
||||||
|
@ -62,6 +62,12 @@ export const SELECTORS = {
|
||||||
previewButtonTestID: 'product-collection-preview-button',
|
previewButtonTestID: 'product-collection-preview-button',
|
||||||
collectionPlaceholder:
|
collectionPlaceholder:
|
||||||
'[data-type="woocommerce/product-collection"] .components-placeholder',
|
'[data-type="woocommerce/product-collection"] .components-placeholder',
|
||||||
|
productPicker: '.wc-blocks-product-collection__editor-product-picker',
|
||||||
|
linkedProductControl: {
|
||||||
|
button: '.wc-block-product-collection-linked-product-control__button',
|
||||||
|
popoverContent:
|
||||||
|
'.wc-block-product-collection-linked-product__popover-content',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Collections =
|
export type Collections =
|
||||||
|
@ -200,10 +206,31 @@ class ProductCollectionPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
pageReference: Page | FrameLocator
|
||||||
|
) {
|
||||||
|
const editorProductPicker = pageReference.locator(
|
||||||
|
SELECTORS.productPicker
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( await editorProductPicker.isVisible() ) {
|
||||||
|
await editorProductPicker
|
||||||
|
.locator( 'label' )
|
||||||
|
.filter( {
|
||||||
|
hasText: 'Album',
|
||||||
|
} )
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async createNewPostAndInsertBlock( collection?: Collections ) {
|
async createNewPostAndInsertBlock( collection?: Collections ) {
|
||||||
await this.admin.createNewPost();
|
await this.admin.createNewPost();
|
||||||
await this.insertProductCollection();
|
await this.insertProductCollection();
|
||||||
await this.chooseCollectionInPost( collection );
|
await this.chooseCollectionInPost( collection );
|
||||||
|
// If product picker is available, choose a product.
|
||||||
|
await this.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
this.admin.page
|
||||||
|
);
|
||||||
await this.refreshLocators( 'editor' );
|
await this.refreshLocators( 'editor' );
|
||||||
await this.editor.openDocumentSettingsSidebar();
|
await this.editor.openDocumentSettingsSidebar();
|
||||||
}
|
}
|
||||||
|
@ -345,6 +372,10 @@ class ProductCollectionPage {
|
||||||
await this.editor.canvas.locator( 'body' ).click();
|
await this.editor.canvas.locator( 'body' ).click();
|
||||||
await this.insertProductCollection();
|
await this.insertProductCollection();
|
||||||
await this.chooseCollectionInTemplate( collection );
|
await this.chooseCollectionInTemplate( collection );
|
||||||
|
// If product picker is available, choose a product.
|
||||||
|
await this.chooseProductInEditorProductPickerIfAvailable(
|
||||||
|
this.editor.canvas
|
||||||
|
);
|
||||||
await this.refreshLocators( 'editor' );
|
await this.refreshLocators( 'editor' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,6 +602,30 @@ class ProductCollectionPage {
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeCollectionUsingToolbar( collection: Collections ) {
|
||||||
|
// Click "Choose collection" button in the toolbar.
|
||||||
|
await this.admin.page
|
||||||
|
.getByRole( 'toolbar', { name: 'Block Tools' } )
|
||||||
|
.getByRole( 'button', { name: 'Choose collection' } )
|
||||||
|
.click();
|
||||||
|
|
||||||
|
// Select the collection from the modal.
|
||||||
|
const collectionChooserModal = this.admin.page.locator(
|
||||||
|
'.wc-blocks-product-collection__modal'
|
||||||
|
);
|
||||||
|
await collectionChooserModal
|
||||||
|
.getByRole( 'button', {
|
||||||
|
name: collectionToButtonNameMap[ collection ],
|
||||||
|
} )
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await collectionChooserModal
|
||||||
|
.getByRole( 'button', {
|
||||||
|
name: 'Continue',
|
||||||
|
} )
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
async setDisplaySettings( {
|
async setDisplaySettings( {
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
offset,
|
offset,
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: major
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Product Collection - Show product picker in Editor when collection requires a product but not available <details> A collection can define if it requires a product context. This can be done using `usesReference` argument i.e. ```tsx __experimentalRegisterProductCollection({ ..., usesReference: ['product'], ) ``` When product context doesn't exist in current template/page/post etc. then we show product picker in Editor. This way, merchant can manually provide a product context to the collection.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: major
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Product Collection - Implement Inspector control to change selected product
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Product bulk edit: fix increasing & decreasing sale price when there was no previous sale.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Product SKU block: add editable prefixes and suffixes.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
Comment: This reverts an existing PR.
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Added doc blocks to conditionally fired hooks to better explain nuanced behavior
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update webpack config to bundle in @wordpress/dataviews package.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Trap focus inside the product gallery modal.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fix variation selector display issues on the front end #51023
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fix My Account block icon being too small when inserted via block hooks
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Add filter for response of `wc_rest_should_load_namespace` function to allow loading namespaces.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Mark several Customize Your Store PHP classes as internal
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Product Collection: Don't render when empty unless the no results block is present.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Add query params masking to remote logger
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Enhance WooCommerce version checking for remote logging reliability
|
|
@ -445,6 +445,18 @@ p.demo_store,
|
||||||
min-width: 75%;
|
min-width: 75%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
|
||||||
|
/* We hide the default chevron because it cannot be directly modified. Instead, we add a custom chevron using a background image. */
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
padding-right: 2em;
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJmZWF0aGVyIGZlYXRoZXItY2hldnJvbi1kb3duIj48cG9seWxpbmUgcG9pbnRzPSI2IDkgMTIgMTUgMTggOSI+PC9wb2x5bGluZT48L3N2Zz4=)
|
||||||
|
no-repeat;
|
||||||
|
background-size: 16px;
|
||||||
|
-webkit-background-size: 16px;
|
||||||
|
background-position: calc(100% - 12px) 50%;
|
||||||
|
-webkit-background-position: calc(100% - 12px) 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.label {
|
td.label {
|
||||||
|
|
|
@ -127,6 +127,8 @@ jQuery( function( $ ) {
|
||||||
this.onResetSlidePosition = this.onResetSlidePosition.bind( this );
|
this.onResetSlidePosition = this.onResetSlidePosition.bind( this );
|
||||||
this.getGalleryItems = this.getGalleryItems.bind( this );
|
this.getGalleryItems = this.getGalleryItems.bind( this );
|
||||||
this.openPhotoswipe = this.openPhotoswipe.bind( this );
|
this.openPhotoswipe = this.openPhotoswipe.bind( this );
|
||||||
|
this.trapFocusPhotoswipe = this.trapFocusPhotoswipe.bind( this );
|
||||||
|
this.handlePswpTrapFocus = this.handlePswpTrapFocus.bind( this );
|
||||||
|
|
||||||
if ( this.flexslider_enabled ) {
|
if ( this.flexslider_enabled ) {
|
||||||
this.initFlexslider( args.flexslider );
|
this.initFlexslider( args.flexslider );
|
||||||
|
@ -307,8 +309,10 @@ jQuery( function( $ ) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
var pswpElement = $( '.pswp' )[0],
|
var pswpElement = $( '.pswp' )[0],
|
||||||
items = this.getGalleryItems(),
|
items = this.getGalleryItems(),
|
||||||
eventTarget = $( e.target ),
|
eventTarget = $( e.target ),
|
||||||
|
currentTarget = e.currentTarget,
|
||||||
|
self = this,
|
||||||
clicked;
|
clicked;
|
||||||
|
|
||||||
if ( 0 < eventTarget.closest( '.woocommerce-product-gallery__trigger' ).length ) {
|
if ( 0 < eventTarget.closest( '.woocommerce-product-gallery__trigger' ).length ) {
|
||||||
|
@ -326,14 +330,73 @@ jQuery( function( $ ) {
|
||||||
}
|
}
|
||||||
captionEl.children[0].textContent = item.title;
|
captionEl.children[0].textContent = item.title;
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
|
timeToIdle: 0, // Ensure the gallery controls are always visible to avoid keyboard navigation issues.
|
||||||
}, wc_single_product_params.photoswipe_options );
|
}, wc_single_product_params.photoswipe_options );
|
||||||
|
|
||||||
// Initializes and opens PhotoSwipe.
|
// Initializes and opens PhotoSwipe.
|
||||||
var photoswipe = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options );
|
var photoswipe = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options );
|
||||||
|
|
||||||
|
photoswipe.listen( 'afterInit', function() {
|
||||||
|
self.trapFocusPhotoswipe( true );
|
||||||
|
});
|
||||||
|
|
||||||
|
photoswipe.listen( 'close', function() {
|
||||||
|
self.trapFocusPhotoswipe( false );
|
||||||
|
currentTarget.focus();
|
||||||
|
});
|
||||||
|
|
||||||
photoswipe.init();
|
photoswipe.init();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control focus in photoswipe modal.
|
||||||
|
*
|
||||||
|
* @param {boolean} trapFocus - Whether to trap focus or not.
|
||||||
|
*/
|
||||||
|
ProductGallery.prototype.trapFocusPhotoswipe = function( trapFocus ) {
|
||||||
|
var pswp = document.querySelector( '.pswp' );
|
||||||
|
|
||||||
|
if ( ! pswp ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( trapFocus ) {
|
||||||
|
pswp.addEventListener( 'keydown', this.handlePswpTrapFocus );
|
||||||
|
} else {
|
||||||
|
pswp.removeEventListener( 'keydown', this.handlePswpTrapFocus );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keydown event in photoswipe modal.
|
||||||
|
*/
|
||||||
|
ProductGallery.prototype.handlePswpTrapFocus = function( e ) {
|
||||||
|
var allFocusablesEls = e.currentTarget.querySelectorAll( 'button:not([disabled])' );
|
||||||
|
var filteredFocusablesEls = Array.from( allFocusablesEls ).filter( function( btn ) {
|
||||||
|
return btn.style.display !== 'none' && window.getComputedStyle( btn ).display !== 'none';
|
||||||
|
} );
|
||||||
|
|
||||||
|
if ( 1 >= filteredFocusablesEls.length ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstTabStop = filteredFocusablesEls[0];
|
||||||
|
var lastTabStop = filteredFocusablesEls[filteredFocusablesEls.length - 1];
|
||||||
|
|
||||||
|
if ( e.key === 'Tab' ) {
|
||||||
|
if ( e.shiftKey ) {
|
||||||
|
if ( document.activeElement === firstTabStop ) {
|
||||||
|
e.preventDefault();
|
||||||
|
lastTabStop.focus();
|
||||||
|
}
|
||||||
|
} else if ( document.activeElement === lastTabStop ) {
|
||||||
|
e.preventDefault();
|
||||||
|
firstTabStop.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to call wc_product_gallery on jquery selector.
|
* Function to call wc_product_gallery on jquery selector.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -896,7 +896,8 @@ class WC_Admin_Post_Types {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$old_price = (float) $product->{"get_{$price_type}_price"}();
|
$old_price = $product->{"get_{$price_type}_price"}();
|
||||||
|
$old_price = '' === $old_price ? (float) $product->get_regular_price() : (float) $old_price;
|
||||||
$price_changed = false;
|
$price_changed = false;
|
||||||
|
|
||||||
$change_price = absint( $request_data[ "change_{$price_type}_price" ] );
|
$change_price = absint( $request_data[ "change_{$price_type}_price" ] );
|
||||||
|
|
|
@ -641,8 +641,13 @@ class WC_Shortcode_Products {
|
||||||
|
|
||||||
do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes );
|
do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes );
|
||||||
|
|
||||||
// Fire standard shop loop hooks when paginating results so we can show result counts and so on.
|
|
||||||
if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
|
if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
|
||||||
|
/**
|
||||||
|
* Fire the standard shop hooks when paginating so we can display result counts etc.
|
||||||
|
* If the pagination is not enabled, this hook will not be fired.
|
||||||
|
*
|
||||||
|
* @since 3.3.1
|
||||||
|
*/
|
||||||
do_action( 'woocommerce_before_shop_loop' );
|
do_action( 'woocommerce_before_shop_loop' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -667,8 +672,13 @@ class WC_Shortcode_Products {
|
||||||
$GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
$GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||||
woocommerce_product_loop_end();
|
woocommerce_product_loop_end();
|
||||||
|
|
||||||
// Fire standard shop loop hooks when paginating results so we can show result counts and so on.
|
|
||||||
if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
|
if ( wc_string_to_bool( $this->attributes['paginate'] ) ) {
|
||||||
|
/**
|
||||||
|
* Fire the standard shop hooks when paginating so we can display the pagination.
|
||||||
|
* If the pagination is not enabled, this hook will not be fired.
|
||||||
|
*
|
||||||
|
* @since 3.3.1
|
||||||
|
*/
|
||||||
do_action( 'woocommerce_after_shop_loop' );
|
do_action( 'woocommerce_after_shop_loop' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -410,8 +410,6 @@ function wc_rest_should_load_namespace( string $ns, string $rest_route = '' ): b
|
||||||
'wc/private',
|
'wc/private',
|
||||||
);
|
);
|
||||||
|
|
||||||
// We can consider allowing filtering this list in the future.
|
|
||||||
|
|
||||||
$known_namespace_request = false;
|
$known_namespace_request = false;
|
||||||
foreach ( $known_namespaces as $known_namespace ) {
|
foreach ( $known_namespaces as $known_namespace ) {
|
||||||
if ( str_starts_with( $rest_route, $known_namespace ) ) {
|
if ( str_starts_with( $rest_route, $known_namespace ) ) {
|
||||||
|
@ -424,5 +422,15 @@ function wc_rest_should_load_namespace( string $ns, string $rest_route = '' ): b
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return str_starts_with( $rest_route, $ns );
|
/**
|
||||||
|
* Filters whether a namespace should be loaded.
|
||||||
|
*
|
||||||
|
* @param bool $should_load True if the namespace should be loaded, false otherwise.
|
||||||
|
* @param string $ns The namespace to check.
|
||||||
|
* @param string $rest_route The REST route being checked.
|
||||||
|
* @param array $known_namespaces Known namespaces that we know are safe to not load if the request is not for them.
|
||||||
|
*
|
||||||
|
* @since 9.4
|
||||||
|
*/
|
||||||
|
return apply_filters( 'wc_rest_should_load_namespace', str_starts_with( $rest_route, $ns ), $ns, $rest_route, $known_namespaces );
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ use WP_Post;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customize Your Store Task
|
* Customize Your Store Task
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class CustomizeStore extends Task {
|
class CustomizeStore extends Task {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,6 +8,8 @@ use Automattic\Jetpack\Connection\Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Configuration
|
* Class Configuration
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class Configuration {
|
class Configuration {
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ use WpOrg\Requests\Requests;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Connection
|
* Class Connection
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class Connection {
|
class Connection {
|
||||||
const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion';
|
const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion';
|
||||||
|
|
|
@ -10,6 +10,8 @@ use WP_Error;
|
||||||
* ContentProcessor class.
|
* ContentProcessor class.
|
||||||
*
|
*
|
||||||
* Process images for content
|
* Process images for content
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class ContentProcessor {
|
class ContentProcessor {
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\Blocks\AIContent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patterns Dictionary class.
|
* Patterns Dictionary class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class PatternsDictionary {
|
class PatternsDictionary {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,6 +6,8 @@ use WP_Error;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patterns Helper class.
|
* Patterns Helper class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class PatternsHelper {
|
class PatternsHelper {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,8 @@ use WP_Error;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pattern Images class.
|
* Pattern Images class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class UpdatePatterns {
|
class UpdatePatterns {
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ use Automattic\WooCommerce\Blocks\AI\Connection;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
/**
|
/**
|
||||||
* Pattern Images class.
|
* Pattern Images class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class UpdateProducts {
|
class UpdateProducts {
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ class CustomerAccount extends AbstractBlock {
|
||||||
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
|
public function modify_hooked_block_attributes( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block, $context ) {
|
||||||
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
|
$parsed_hooked_block['attrs']['displayStyle'] = 'icon_only';
|
||||||
$parsed_hooked_block['attrs']['iconStyle'] = 'line';
|
$parsed_hooked_block['attrs']['iconStyle'] = 'line';
|
||||||
|
$parsed_hooked_block['attrs']['iconClass'] = 'wc-block-customer-account__account-icon';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.
|
* The Mini Cart block (which is hooked into the header) has a margin of 0.5em on the left side.
|
||||||
|
|
|
@ -47,6 +47,18 @@ class ProductCollection extends AbstractBlock {
|
||||||
protected $custom_order_opts = array( 'popularity', 'rating' );
|
protected $custom_order_opts = array( 'popularity', 'rating' );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The render state of the product collection block.
|
||||||
|
*
|
||||||
|
* These props are runtime-based and reinitialize for every block on a page.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $render_state = array(
|
||||||
|
'has_results' => false,
|
||||||
|
'has_no_results_block' => false,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize this block type.
|
* Initialize this block type.
|
||||||
*
|
*
|
||||||
|
@ -80,8 +92,30 @@ class ProductCollection extends AbstractBlock {
|
||||||
// Provide location context into block's context.
|
// Provide location context into block's context.
|
||||||
add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 );
|
add_filter( 'render_block_context', array( $this, 'provide_location_context_for_inner_blocks' ), 11, 1 );
|
||||||
|
|
||||||
|
// Disable block render if the ProductTemplate block is empty.
|
||||||
|
add_filter(
|
||||||
|
'render_block_woocommerce/product-template',
|
||||||
|
function ( $html ) {
|
||||||
|
$this->render_state['has_results'] = ! empty( $html );
|
||||||
|
return $html;
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable block render if the ProductCollectionNoResults block is rendered.
|
||||||
|
add_filter(
|
||||||
|
'render_block_woocommerce/product-collection-no-results',
|
||||||
|
function ( $html ) {
|
||||||
|
$this->render_state['has_no_results_block'] = ! empty( $html );
|
||||||
|
return $html;
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
// Interactivity API: Add navigation directives to the product collection block.
|
// Interactivity API: Add navigation directives to the product collection block.
|
||||||
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
|
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'handle_rendering' ), 10, 2 );
|
||||||
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
||||||
|
|
||||||
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
||||||
|
@ -90,6 +124,46 @@ class ProductCollection extends AbstractBlock {
|
||||||
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
|
add_filter( 'render_block_data', array( $this, 'disable_enhanced_pagination' ), 10, 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the rendering of the block.
|
||||||
|
*
|
||||||
|
* @param string $block_content The block content about to be rendered.
|
||||||
|
* @param array $block The block being rendered.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function handle_rendering( $block_content, $block ) {
|
||||||
|
if ( $this->should_prevent_render() ) {
|
||||||
|
return ''; // Prevent rendering.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the render state for the next render.
|
||||||
|
$this->reset_render_state();
|
||||||
|
|
||||||
|
return $this->enhance_product_collection_with_interactivity( $block_content, $block );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the block should be prevented from rendering.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function should_prevent_render() {
|
||||||
|
return ! $this->render_state['has_results'] && ! $this->render_state['has_no_results_block'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the render state.
|
||||||
|
*/
|
||||||
|
private function reset_render_state() {
|
||||||
|
$this->render_state = array(
|
||||||
|
'has_results' => false,
|
||||||
|
'has_no_results_block' => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the location context to each inner block of the product collection block.
|
* Provides the location context to each inner block of the product collection block.
|
||||||
* Hint: Only blocks using the 'query' context will be affected.
|
* Hint: Only blocks using the 'query' context will be affected.
|
||||||
|
|
|
@ -69,14 +69,27 @@ class ProductSKU extends AbstractBlock {
|
||||||
|
|
||||||
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
|
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
|
||||||
|
|
||||||
|
$prefix = isset( $attributes['prefix'] ) ? wp_kses_post( ( $attributes['prefix'] ) ) : __( 'SKU: ', 'woocommerce' );
|
||||||
|
if ( ! empty( $prefix ) ) {
|
||||||
|
$prefix = sprintf( '<span class="prefix">%s</span>', $prefix );
|
||||||
|
}
|
||||||
|
|
||||||
|
$suffix = isset( $attributes['suffix'] ) ? wp_kses_post( ( $attributes['suffix'] ) ) : '';
|
||||||
|
if ( ! empty( $suffix ) ) {
|
||||||
|
$suffix = sprintf( '<span class="suffix">%s</span>', $suffix );
|
||||||
|
}
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<div class="wc-block-components-product-sku wc-block-grid__product-sku wp-block-woocommerce-product-sku product_meta %1$s" style="%2$s">
|
'<div class="wc-block-components-product-sku wc-block-grid__product-sku wp-block-woocommerce-product-sku product_meta %1$s" style="%2$s">
|
||||||
SKU:
|
%3$s
|
||||||
<strong class="sku">%3$s</strong>
|
<strong class="sku">%4$s</strong>
|
||||||
|
%5$s
|
||||||
</div>',
|
</div>',
|
||||||
esc_attr( $styles_and_classes['classes'] ),
|
esc_attr( $styles_and_classes['classes'] ),
|
||||||
esc_attr( $styles_and_classes['styles'] ?? '' ),
|
esc_attr( $styles_and_classes['styles'] ?? '' ),
|
||||||
$product_sku
|
$prefix,
|
||||||
|
$product_sku,
|
||||||
|
$suffix
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ use Automattic\WooCommerce\Blocks\Images\Pexels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AIPatterns class.
|
* AIPatterns class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class AIPatterns {
|
class AIPatterns {
|
||||||
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';
|
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';
|
||||||
|
|
|
@ -5,6 +5,8 @@ use WP_Error;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PatternsToolkit class.
|
* PatternsToolkit class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class PTKClient {
|
class PTKClient {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,8 @@ use WP_Upgrader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PTKPatterns class.
|
* PTKPatterns class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class PTKPatternsStore {
|
class PTKPatternsStore {
|
||||||
const TRANSIENT_NAME = 'ptk_patterns';
|
const TRANSIENT_NAME = 'ptk_patterns';
|
||||||
|
|
|
@ -5,6 +5,8 @@ use Automattic\WooCommerce\Admin\Features\Features;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PatternRegistry class.
|
* PatternRegistry class.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
class PatternRegistry {
|
class PatternRegistry {
|
||||||
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
||||||
|
|
|
@ -20,11 +20,10 @@ use WC_Log_Levels;
|
||||||
* @package WooCommerce\Classes
|
* @package WooCommerce\Classes
|
||||||
*/
|
*/
|
||||||
class RemoteLogger extends \WC_Log_Handler {
|
class RemoteLogger extends \WC_Log_Handler {
|
||||||
const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash';
|
const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash';
|
||||||
const RATE_LIMIT_ID = 'woocommerce_remote_logging';
|
const RATE_LIMIT_ID = 'woocommerce_remote_logging';
|
||||||
const RATE_LIMIT_DELAY = 60; // 1 minute.
|
const RATE_LIMIT_DELAY = 60; // 1 minute.
|
||||||
const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version';
|
const WC_NEW_VERSION_TRANSIENT = 'woocommerce_new_version';
|
||||||
const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a log entry.
|
* Handle a log entry.
|
||||||
|
@ -71,7 +70,7 @@ class RemoteLogger extends \WC_Log_Handler {
|
||||||
'wc_version' => WC()->version,
|
'wc_version' => WC()->version,
|
||||||
'php_version' => phpversion(),
|
'php_version' => phpversion(),
|
||||||
'wp_version' => get_bloginfo( 'version' ),
|
'wp_version' => get_bloginfo( 'version' ),
|
||||||
'request_uri' => filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ),
|
'request_uri' => $this->sanitize_request_uri( filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ) ),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -150,7 +149,7 @@ class RemoteLogger extends \WC_Log_Handler {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! $this->is_latest_woocommerce_version() ) {
|
if ( ! $this->should_current_version_be_logged() ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,7 +220,7 @@ class RemoteLogger extends \WC_Log_Handler {
|
||||||
self::LOG_ENDPOINT,
|
self::LOG_ENDPOINT,
|
||||||
array(
|
array(
|
||||||
'body' => wp_json_encode( $body ),
|
'body' => wp_json_encode( $body ),
|
||||||
'timeout' => 2,
|
'timeout' => 3,
|
||||||
'headers' => array(
|
'headers' => array(
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json',
|
||||||
),
|
),
|
||||||
|
@ -256,14 +255,22 @@ class RemoteLogger extends \WC_Log_Handler {
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function is_latest_woocommerce_version() {
|
private function should_current_version_be_logged() {
|
||||||
$latest_wc_version = $this->fetch_latest_woocommerce_version();
|
$new_version = get_site_transient( self::WC_NEW_VERSION_TRANSIENT );
|
||||||
|
|
||||||
if ( is_null( $latest_wc_version ) ) {
|
if ( false === $new_version ) {
|
||||||
return false;
|
$new_version = $this->fetch_new_woocommerce_version();
|
||||||
|
// Cache the new version for a week since we want to keep logging in with the same version for a while even if the new version is available.
|
||||||
|
set_site_transient( self::WC_NEW_VERSION_TRANSIENT, $new_version, WEEK_IN_SECONDS );
|
||||||
}
|
}
|
||||||
|
|
||||||
return version_compare( WC()->version, $latest_wc_version, '>=' );
|
if ( ! is_string( $new_version ) || '' === $new_version ) {
|
||||||
|
// If the new version is not available, we consider the current version to be the latest.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current version is the latest, we don't want to log errors.
|
||||||
|
return version_compare( WC()->version, $new_version, '>=' );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -316,45 +323,34 @@ class RemoteLogger extends \WC_Log_Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the latest WooCommerce version using the WordPress API and cache it.
|
* Fetch the new version of WooCommerce from the WordPress API.
|
||||||
*
|
*
|
||||||
* @return string|null
|
* @return string|null New version if an update is available, null otherwise.
|
||||||
*/
|
*/
|
||||||
private function fetch_latest_woocommerce_version() {
|
private function fetch_new_woocommerce_version() {
|
||||||
$cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT );
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
if ( $cached_version ) {
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
return $cached_version;
|
}
|
||||||
|
if ( ! function_exists( 'get_plugin_updates' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/update.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
$retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY );
|
$plugin_updates = get_plugin_updates();
|
||||||
if ( false === $retry_count || ! is_numeric( $retry_count ) ) {
|
|
||||||
$retry_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $retry_count >= 3 ) {
|
// Check if WooCommerce plugin update information is available.
|
||||||
|
if ( ! is_array( $plugin_updates ) || ! isset( $plugin_updates[ WC_PLUGIN_BASENAME ] ) ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! function_exists( 'plugins_api' ) ) {
|
$wc_plugin_update = $plugin_updates[ WC_PLUGIN_BASENAME ];
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
|
|
||||||
}
|
|
||||||
// Fetch the latest version from the WordPress API.
|
|
||||||
$plugin_info = plugins_api( 'plugin_information', array( 'slug' => 'woocommerce' ) );
|
|
||||||
|
|
||||||
if ( is_wp_error( $plugin_info ) ) {
|
// Ensure the update object exists and has the required information.
|
||||||
++$retry_count;
|
if ( ! $wc_plugin_update || ! isset( $wc_plugin_update->update->new_version ) ) {
|
||||||
set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS );
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! empty( $plugin_info->version ) ) {
|
$new_version = $wc_plugin_update->update->new_version;
|
||||||
$latest_version = $plugin_info->version;
|
return is_string( $new_version ) ? $new_version : null;
|
||||||
set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS );
|
|
||||||
delete_transient( self::FETCH_LATEST_VERSION_RETRY );
|
|
||||||
return $latest_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -435,4 +431,52 @@ class RemoteLogger extends \WC_Log_Handler {
|
||||||
protected function is_dev_or_local_environment() {
|
protected function is_dev_or_local_environment() {
|
||||||
return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
|
return in_array( wp_get_environment_type(), array( 'development', 'local' ), true );
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Sanitize the request URI to only allow certain query parameters.
|
||||||
|
*
|
||||||
|
* @param string $request_uri The request URI to sanitize.
|
||||||
|
* @return string The sanitized request URI.
|
||||||
|
*/
|
||||||
|
private function sanitize_request_uri( $request_uri ) {
|
||||||
|
$default_whitelist = array( 'path', 'page', 'step', 'task', 'tab' );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter to allow other plugins to whitelist request_uri query parameter values for unmasked remote logging.
|
||||||
|
*
|
||||||
|
* @since 9.4.0
|
||||||
|
*
|
||||||
|
* @param string $default_whitelist The default whitelist of query parameters.
|
||||||
|
*/
|
||||||
|
$whitelist = apply_filters( 'woocommerce_remote_logger_request_uri_whitelist', $default_whitelist );
|
||||||
|
|
||||||
|
$parsed_url = wp_parse_url( $request_uri );
|
||||||
|
if ( ! isset( $parsed_url['query'] ) ) {
|
||||||
|
return $request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str( $parsed_url['query'], $query_params );
|
||||||
|
|
||||||
|
foreach ( $query_params as $key => &$value ) {
|
||||||
|
if ( ! in_array( $key, $whitelist, true ) ) {
|
||||||
|
$value = 'xxxxxx';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed_url['query'] = http_build_query( $query_params );
|
||||||
|
return $this->build_url( $parsed_url );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a URL from its parsed components.
|
||||||
|
*
|
||||||
|
* @param array $parsed_url The parsed URL components.
|
||||||
|
* @return string The built URL.
|
||||||
|
*/
|
||||||
|
private function build_url( $parsed_url ) {
|
||||||
|
$path = $parsed_url['path'] ?? '';
|
||||||
|
$query = isset( $parsed_url['query'] ) ? "?{$parsed_url['query']}" : '';
|
||||||
|
$fragment = isset( $parsed_url['fragment'] ) ? "#{$parsed_url['fragment']}" : '';
|
||||||
|
|
||||||
|
return "$path$query$fragment";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,9 +60,8 @@ trait DraftOrderTrait {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failed orders and those needing payment can be retried if the cart hasn't changed.
|
// Pending and failed orders can be retried if the cart hasn't changed.
|
||||||
// Pending orders are excluded from this check since they may be awaiting an update from the payment processor.
|
if ( $order_object->needs_payment() && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) {
|
||||||
if ( $order_object->needs_payment() && ! $order_object->has_status( 'pending' ) && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -282,3 +282,119 @@ test(
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'can decrease the sale price if the product was not previously in sale when bulk editing products',
|
||||||
|
{ tag: [ '@gutenberg', '@services' ] },
|
||||||
|
async ( { page, products } ) => {
|
||||||
|
await page.goto( `wp-admin/edit.php?post_type=product` );
|
||||||
|
|
||||||
|
const salePriceDecrease = 10;
|
||||||
|
|
||||||
|
await test.step( 'Update products with the "Sale > Decrease existing sale price" option', async () => {
|
||||||
|
await page.goto( `wp-admin/edit.php?post_type=product` );
|
||||||
|
|
||||||
|
for ( const product of products ) {
|
||||||
|
await page.getByLabel( `Select ${ product.name }` ).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator( '#bulk-action-selector-top' )
|
||||||
|
.selectOption( 'Edit' );
|
||||||
|
await page.locator( '#doaction' ).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator( 'select[name="change_sale_price"]' )
|
||||||
|
.selectOption(
|
||||||
|
'Decrease existing sale price by (fixed amount or %):'
|
||||||
|
);
|
||||||
|
await page
|
||||||
|
.getByPlaceholder( 'Enter sale price ($)' )
|
||||||
|
.fill( `${ salePriceDecrease }%` );
|
||||||
|
|
||||||
|
await page.getByRole( 'button', { name: 'Update' } ).click();
|
||||||
|
} );
|
||||||
|
|
||||||
|
await test.step( 'Verify products have a sale price', async () => {
|
||||||
|
for ( const product of products ) {
|
||||||
|
await page.goto( `product/${ product.slug }` );
|
||||||
|
|
||||||
|
const expectedSalePrice = (
|
||||||
|
product.regular_price *
|
||||||
|
( 1 - salePriceDecrease / 100 )
|
||||||
|
).toFixed( 2 );
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.soft(
|
||||||
|
await page
|
||||||
|
.locator( 'ins' )
|
||||||
|
.getByText( `$${ expectedSalePrice }` )
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
.toBeGreaterThan( 0 );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'increasing the sale price from 0 does not change the sale price when bulk editing products',
|
||||||
|
{ tag: [ '@gutenberg', '@services' ] },
|
||||||
|
async ( { page, api } ) => {
|
||||||
|
let product;
|
||||||
|
await api
|
||||||
|
.post( 'products', {
|
||||||
|
id: 0,
|
||||||
|
name: `Product _${ Date.now() }`,
|
||||||
|
type: 'simple',
|
||||||
|
regular_price: '100',
|
||||||
|
sale_price: '0',
|
||||||
|
manage_stock: true,
|
||||||
|
stock_quantity: 10,
|
||||||
|
stock_status: 'instock',
|
||||||
|
} )
|
||||||
|
.then( ( response ) => {
|
||||||
|
product = response.data;
|
||||||
|
} );
|
||||||
|
|
||||||
|
const salePriceIncrease = 10;
|
||||||
|
|
||||||
|
await test.step( 'Update products with the "Sale > Increase existing sale price" option', async () => {
|
||||||
|
await page.goto( `wp-admin/edit.php?post_type=product` );
|
||||||
|
|
||||||
|
await page.getByLabel( `Select ${ product.name }` ).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator( '#bulk-action-selector-top' )
|
||||||
|
.selectOption( 'Edit' );
|
||||||
|
await page.locator( '#doaction' ).click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator( 'select[name="change_sale_price"]' )
|
||||||
|
.selectOption(
|
||||||
|
'Increase existing sale price by (fixed amount or %):'
|
||||||
|
);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByPlaceholder( 'Enter sale price ($)' )
|
||||||
|
.fill( `${ salePriceIncrease }%` );
|
||||||
|
|
||||||
|
await page.getByRole( 'button', { name: 'Update' } ).click();
|
||||||
|
} );
|
||||||
|
|
||||||
|
await test.step( 'Verify products have a sale price', async () => {
|
||||||
|
await page.goto( `product/${ product.slug }` );
|
||||||
|
|
||||||
|
const expectedSalePrice = '$0.00';
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.soft(
|
||||||
|
await page
|
||||||
|
.locator( 'ins' )
|
||||||
|
.getByText( expectedSalePrice )
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
.toBeGreaterThan( 0 );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -27,4 +27,22 @@ class WCRestFunctionsTest extends WC_Unit_Test_Case {
|
||||||
$this->assertFalse( wc_rest_should_load_namespace( 'wc-analytics', 'wc/v2' ) );
|
$this->assertFalse( wc_rest_should_load_namespace( 'wc-analytics', 'wc/v2' ) );
|
||||||
$this->assertTrue( wc_rest_should_load_namespace( 'wc/v2', 'wc/v2' ) );
|
$this->assertTrue( wc_rest_should_load_namespace( 'wc/v2', 'wc/v2' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @testDox Test wc_rest_should_load_namespace known works with preload.
|
||||||
|
*/
|
||||||
|
public function test_wc_rest_should_load_namespace_known_works_with_preload() {
|
||||||
|
$memo = rest_preload_api_request( array(), '/wc/store/v1/cart' );
|
||||||
|
$this->assertArrayHasKey( '/wc/store/v1/cart', $memo );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @testDox Test wc_rest_should_load_namespace filter.
|
||||||
|
*/
|
||||||
|
public function test_wc_rest_should_load_namespace_filter() {
|
||||||
|
$this->assertFalse( wc_rest_should_load_namespace( 'wc/v1', 'wc/v2' ) );
|
||||||
|
add_filter( 'wc_rest_should_load_namespace', '__return_true' );
|
||||||
|
$this->assertTrue( wc_rest_should_load_namespace( 'wc/v1', 'wc/v2' ) );
|
||||||
|
remove_filter( 'wc_rest_should_load_namespace', '__return_true' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
1242
pnpm-lock.yaml
1242
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue