Update lockfile
This commit is contained in:
commit
e01c3749d9
|
@ -201,7 +201,9 @@
|
|||
"@wordpress/interface",
|
||||
"@wordpress/router",
|
||||
"@wordpress/edit-site",
|
||||
"@wordpress/private-apis"
|
||||
"@wordpress/private-apis",
|
||||
"@wordpress/dataviews",
|
||||
"@wordpress/icons"
|
||||
],
|
||||
"packages": [
|
||||
"@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/core-data": "wp-6.0",
|
||||
"@wordpress/data": "wp-6.0",
|
||||
"@wordpress/dataviews": "^4.2.0",
|
||||
"@wordpress/date": "wp-6.0",
|
||||
"@wordpress/deprecated": "wp-6.0",
|
||||
"@wordpress/edit-post": "wp-6.0",
|
||||
|
@ -64,7 +65,7 @@
|
|||
"@wordpress/hooks": "wp-6.0",
|
||||
"@wordpress/html-entities": "wp-6.0",
|
||||
"@wordpress/i18n": "wp-6.0",
|
||||
"@wordpress/icons": "wp-6.0",
|
||||
"@wordpress/icons": "10.6.0",
|
||||
"@wordpress/interface": "wp-6.0",
|
||||
"@wordpress/keyboard-shortcuts": "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
|
||||
*/
|
||||
import { unlock } from '../lock-unlock';
|
||||
import useLayoutAreas from './router';
|
||||
import { Layout } from './layout';
|
||||
|
||||
const { RouterProvider } = unlock( routerPrivateApis );
|
||||
const { GlobalStylesProvider } = unlock( editorPrivateApis );
|
||||
|
||||
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() {
|
||||
|
|
|
@ -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 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.woocommerce_page_woocommerce-products-dashboard #wpcontent {
|
||||
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 reset;
|
||||
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;
|
||||
hasResolved: boolean;
|
||||
};
|
||||
const store: string;
|
||||
}
|
||||
declare module '@wordpress/keyboard-shortcuts' {
|
||||
function useShortcut(
|
||||
|
@ -43,3 +44,24 @@ declare module '@wordpress/keyboard-shortcuts' {
|
|||
declare module '@wordpress/router' {
|
||||
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' ],
|
||||
alias: {
|
||||
'~': path.resolve( __dirname + '/client' ),
|
||||
'react/jsx-dev-runtime': require.resolve( 'react/jsx-dev-runtime' ),
|
||||
'react/jsx-runtime': require.resolve( 'react/jsx-runtime' ),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
@ -240,6 +242,10 @@ const webpackConfig = {
|
|||
return null;
|
||||
}
|
||||
|
||||
if ( request.startsWith( '@wordpress/dataviews' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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.
|
||||
// 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_allow_tracking', 'yes' );
|
||||
update_option( 'woocommerce_remote_variant_assignment', 1 );
|
||||
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, WC()->version );
|
||||
} else {
|
||||
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
|
|
@ -4,7 +4,9 @@
|
|||
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
const Save = () => {
|
||||
const blockProps = useBlockProps.save();
|
||||
const blockProps = useBlockProps.save( {
|
||||
className: 'woocommerce',
|
||||
} );
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
|
|
|
@ -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: patch
|
||||
Type: tweak
|
||||
Comment: This reverts an existing PR.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update webpack config to bundle in @wordpress/dataviews package.
|
|
@ -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: dev
|
||||
|
||||
Mark several Customize Your Store PHP classes as internal
|
|
@ -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%;
|
||||
display: inline-block;
|
||||
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 {
|
||||
|
|
|
@ -896,7 +896,8 @@ class WC_Admin_Post_Types {
|
|||
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;
|
||||
|
||||
$change_price = absint( $request_data[ "change_{$price_type}_price" ] );
|
||||
|
|
|
@ -7,6 +7,8 @@ use WP_Post;
|
|||
|
||||
/**
|
||||
* Customize Your Store Task
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CustomizeStore extends Task {
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,8 @@ use Automattic\Jetpack\Connection\Utils;
|
|||
|
||||
/**
|
||||
* Class Configuration
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Configuration {
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ use WpOrg\Requests\Requests;
|
|||
|
||||
/**
|
||||
* Class Connection
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class Connection {
|
||||
const TEXT_COMPLETION_API_URL = 'https://public-api.wordpress.com/wpcom/v2/text-completion';
|
||||
|
|
|
@ -10,6 +10,8 @@ use WP_Error;
|
|||
* ContentProcessor class.
|
||||
*
|
||||
* Process images for content
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ContentProcessor {
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ namespace Automattic\WooCommerce\Blocks\AIContent;
|
|||
|
||||
/**
|
||||
* Patterns Dictionary class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PatternsDictionary {
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,8 @@ use WP_Error;
|
|||
|
||||
/**
|
||||
* Patterns Helper class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PatternsHelper {
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,8 @@ use WP_Error;
|
|||
|
||||
/**
|
||||
* Pattern Images class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UpdatePatterns {
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ use Automattic\WooCommerce\Blocks\AI\Connection;
|
|||
use WP_Error;
|
||||
/**
|
||||
* Pattern Images class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UpdateProducts {
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ use Automattic\WooCommerce\Blocks\Images\Pexels;
|
|||
|
||||
/**
|
||||
* AIPatterns class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AIPatterns {
|
||||
const PATTERNS_AI_DATA_POST_TYPE = 'patterns_ai_data';
|
||||
|
|
|
@ -5,6 +5,8 @@ use WP_Error;
|
|||
|
||||
/**
|
||||
* PatternsToolkit class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PTKClient {
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,8 @@ use WP_Upgrader;
|
|||
|
||||
/**
|
||||
* PTKPatterns class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PTKPatternsStore {
|
||||
const TRANSIENT_NAME = 'ptk_patterns';
|
||||
|
|
|
@ -5,6 +5,8 @@ use Automattic\WooCommerce\Admin\Features\Features;
|
|||
|
||||
/**
|
||||
* PatternRegistry class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PatternRegistry {
|
||||
const SLUG_REGEX = '/^[A-z0-9\/_-]+$/';
|
||||
|
|
|
@ -20,11 +20,10 @@ use WC_Log_Levels;
|
|||
* @package WooCommerce\Classes
|
||||
*/
|
||||
class RemoteLogger extends \WC_Log_Handler {
|
||||
const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash';
|
||||
const RATE_LIMIT_ID = 'woocommerce_remote_logging';
|
||||
const RATE_LIMIT_DELAY = 60; // 1 minute.
|
||||
const WC_LATEST_VERSION_TRANSIENT = 'latest_woocommerce_version';
|
||||
const FETCH_LATEST_VERSION_RETRY = 'fetch_latest_woocommerce_version_retry';
|
||||
const LOG_ENDPOINT = 'https://public-api.wordpress.com/rest/v1.1/logstash';
|
||||
const RATE_LIMIT_ID = 'woocommerce_remote_logging';
|
||||
const RATE_LIMIT_DELAY = 60; // 1 minute.
|
||||
const WC_NEW_VERSION_TRANSIENT = 'woocommerce_new_version';
|
||||
|
||||
/**
|
||||
* Handle a log entry.
|
||||
|
@ -150,7 +149,7 @@ class RemoteLogger extends \WC_Log_Handler {
|
|||
return false;
|
||||
}
|
||||
|
||||
if ( ! $this->is_latest_woocommerce_version() ) {
|
||||
if ( ! $this->should_current_version_be_logged() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -221,7 +220,7 @@ class RemoteLogger extends \WC_Log_Handler {
|
|||
self::LOG_ENDPOINT,
|
||||
array(
|
||||
'body' => wp_json_encode( $body ),
|
||||
'timeout' => 2,
|
||||
'timeout' => 3,
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
|
@ -256,14 +255,22 @@ class RemoteLogger extends \WC_Log_Handler {
|
|||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_latest_woocommerce_version() {
|
||||
$latest_wc_version = $this->fetch_latest_woocommerce_version();
|
||||
private function should_current_version_be_logged() {
|
||||
$new_version = get_site_transient( self::WC_NEW_VERSION_TRANSIENT );
|
||||
|
||||
if ( is_null( $latest_wc_version ) ) {
|
||||
return false;
|
||||
if ( false === $new_version ) {
|
||||
$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() {
|
||||
$cached_version = get_transient( self::WC_LATEST_VERSION_TRANSIENT );
|
||||
if ( $cached_version ) {
|
||||
return $cached_version;
|
||||
private function fetch_new_woocommerce_version() {
|
||||
if ( ! function_exists( 'get_plugins' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
if ( ! function_exists( 'get_plugin_updates' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/update.php';
|
||||
}
|
||||
|
||||
$retry_count = get_transient( self::FETCH_LATEST_VERSION_RETRY );
|
||||
if ( false === $retry_count || ! is_numeric( $retry_count ) ) {
|
||||
$retry_count = 0;
|
||||
}
|
||||
$plugin_updates = get_plugin_updates();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'plugins_api' ) ) {
|
||||
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' ) );
|
||||
$wc_plugin_update = $plugin_updates[ WC_PLUGIN_BASENAME ];
|
||||
|
||||
if ( is_wp_error( $plugin_info ) ) {
|
||||
++$retry_count;
|
||||
set_transient( self::FETCH_LATEST_VERSION_RETRY, $retry_count, HOUR_IN_SECONDS );
|
||||
// Ensure the update object exists and has the required information.
|
||||
if ( ! $wc_plugin_update || ! isset( $wc_plugin_update->update->new_version ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! empty( $plugin_info->version ) ) {
|
||||
$latest_version = $plugin_info->version;
|
||||
set_transient( self::WC_LATEST_VERSION_TRANSIENT, $latest_version, WEEK_IN_SECONDS );
|
||||
delete_transient( self::FETCH_LATEST_VERSION_RETRY );
|
||||
return $latest_version;
|
||||
}
|
||||
|
||||
return null;
|
||||
$new_version = $wc_plugin_update->update->new_version;
|
||||
return is_string( $new_version ) ? $new_version : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,9 +60,8 @@ trait DraftOrderTrait {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Failed orders and those needing payment 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_status( 'pending' ) && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) {
|
||||
// Pending and failed orders can be retried if the cart hasn't changed.
|
||||
if ( $order_object->needs_payment() && $order_object->has_cart_hash( wc()->cart->get_cart_hash() ) ) {
|
||||
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 );
|
||||
} );
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,8 +35,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
public function tearDown(): void {
|
||||
$this->cleanup_filters();
|
||||
delete_option( 'woocommerce_feature_remote_logging_enabled' );
|
||||
delete_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT );
|
||||
delete_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY );
|
||||
delete_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
|
||||
global $wpdb;
|
||||
$wpdb->query( "DELETE FROM {$wpdb->prefix}wc_rate_limits" );
|
||||
WC_Cache_Helper::invalidate_cache_group( WC_Rate_Limiter::CACHE_GROUP );
|
||||
|
@ -56,6 +55,7 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
'plugins_api',
|
||||
'pre_http_request',
|
||||
'woocommerce_remote_logger_formatted_log_data',
|
||||
'pre_site_transient_update_plugins',
|
||||
);
|
||||
foreach ( $filters as $filter ) {
|
||||
remove_all_filters( $filter );
|
||||
|
@ -90,18 +90,23 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
*/
|
||||
public function remote_logging_disallowed_provider() {
|
||||
return array(
|
||||
'feature flag disabled' => array(
|
||||
'feature flag disabled' => array(
|
||||
'condition' => 'feature flag disabled',
|
||||
'setup' => fn() => update_option( 'woocommerce_feature_remote_logging_enabled', 'no' ),
|
||||
),
|
||||
'tracking opted out' => array(
|
||||
'tracking opted out' => array(
|
||||
'condition' => 'tracking opted out',
|
||||
'setup' => fn() => add_filter( 'option_woocommerce_allow_tracking', fn() => 'no' ),
|
||||
),
|
||||
'outdated version' => array(
|
||||
'condition' => 'outdated version',
|
||||
'setup' => function () {
|
||||
'high variant assignment' => array(
|
||||
'condition' => 'high variant assignment',
|
||||
'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ),
|
||||
),
|
||||
'outdated version' => array(
|
||||
'condition' => 'outdated version',
|
||||
'setup' => function () {
|
||||
$version = WC()->version;
|
||||
// Next major version. (e.g. 9.0.1 -> 10.0.0).
|
||||
$next_version = implode(
|
||||
'.',
|
||||
array_map(
|
||||
|
@ -112,28 +117,79 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
array_keys( explode( '.', $version ) )
|
||||
)
|
||||
);
|
||||
set_transient( RemoteLogger::WC_LATEST_VERSION_TRANSIENT, $next_version );
|
||||
|
||||
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $next_version, WEEK_IN_SECONDS );
|
||||
},
|
||||
'high variant assignment' => array(
|
||||
'condition' => 'high variant assignment',
|
||||
'setup' => fn() => add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 15 ),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox Fetch latest WooCommerce version retries on API failure
|
||||
*/
|
||||
public function test_fetch_latest_woocommerce_version_retry() {
|
||||
$this->setup_remote_logging_conditions( true );
|
||||
add_filter( 'plugins_api', fn() => new \WP_Error(), 10, 3 );
|
||||
|
||||
for ( $i = 1; $i <= 4; $i++ ) {
|
||||
$this->sut->is_remote_logging_allowed();
|
||||
$retry_count = get_transient( RemoteLogger::FETCH_LATEST_VERSION_RETRY );
|
||||
$this->assertEquals( min( $i, 3 ), $retry_count );
|
||||
/**
|
||||
* @testdox should_current_version_be_logged method behaves correctly
|
||||
* @dataProvider should_current_version_be_logged_provider
|
||||
*
|
||||
* @param string $current_version The current WooCommerce version.
|
||||
* @param string $new_version The new WooCommerce version.
|
||||
* @param string $transient_value The value of the transient.
|
||||
* @param bool $expected The expected result.
|
||||
*/
|
||||
public function test_should_current_version_be_logged( $current_version, $new_version, $transient_value, $expected ) {
|
||||
$wc_version = WC()->version;
|
||||
WC()->version = $current_version;
|
||||
|
||||
// Set up the transient.
|
||||
if ( null !== $transient_value ) {
|
||||
set_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT, $transient_value, WEEK_IN_SECONDS );
|
||||
} else {
|
||||
delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
|
||||
|
||||
$this->setup_mock_plugin_updates( $new_version );
|
||||
}
|
||||
|
||||
$result = $this->invoke_private_method( $this->sut, 'should_current_version_be_logged', array() );
|
||||
$this->assertEquals( $expected, $result );
|
||||
|
||||
// Clean up.
|
||||
delete_site_transient( RemoteLogger::WC_NEW_VERSION_TRANSIENT );
|
||||
|
||||
WC()->version = $wc_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_should_current_version_be_logged.
|
||||
*/
|
||||
public function should_current_version_be_logged_provider() {
|
||||
return array(
|
||||
'current version is latest (transient set)' => array( '9.2.0', '9.2.0', '9.2.0', true ),
|
||||
'current version is newer (transient set)' => array( '9.3.0', '9.2.0', '9.2.0', true ),
|
||||
'current version is older (transient set)' => array( '9.1.0', '9.2.0', '9.2.0', false ),
|
||||
'new version is null (transient set)' => array( '9.2.0', null, null, true ),
|
||||
'transient not set, current version is latest' => array( '9.2.0', '9.2.0', null, true ),
|
||||
'transient not set, current version is newer' => array( '9.3.0', '9.2.0', null, true ),
|
||||
'transient not set, current version is older' => array( '9.1.0', '9.2.0', null, false ),
|
||||
'transient not set, new version is null' => array( '9.2.0', null, null, true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox fetch_new_woocommerce_version method returns correct version
|
||||
*/
|
||||
public function test_fetch_new_woocommerce_version() {
|
||||
$this->setup_mock_plugin_updates( '9.3.0' );
|
||||
|
||||
$result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() );
|
||||
$this->assertEquals( '9.3.0', $result, 'The result should be the latest version when an update is available.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @testdox fetch_new_woocommerce_version method returns null when no update is available
|
||||
*/
|
||||
public function test_fetch_new_woocommerce_version_no_update() {
|
||||
add_filter( 'pre_site_transient_update_plugins', fn() => array() );
|
||||
|
||||
$result = $this->invoke_private_method( $this->sut, 'fetch_new_woocommerce_version', array() );
|
||||
$this->assertNull( $result, 'The result should be null when no update is available.' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -421,17 +477,26 @@ class RemoteLoggerTest extends \WC_Unit_Test_Case {
|
|||
update_option( 'woocommerce_feature_remote_logging_enabled', $enabled ? 'yes' : 'no' );
|
||||
add_filter( 'option_woocommerce_allow_tracking', fn() => 'yes' );
|
||||
add_filter( 'option_woocommerce_remote_variant_assignment', fn() => 5 );
|
||||
add_filter(
|
||||
'plugins_api',
|
||||
function ( $result, $action, $args ) use ( $enabled ) {
|
||||
if ( 'plugin_information' === $action && 'woocommerce' === $args->slug ) {
|
||||
return (object) array( 'version' => $enabled ? WC()->version : '9.0.0' );
|
||||
}
|
||||
return $result;
|
||||
},
|
||||
10,
|
||||
3
|
||||
$this->setup_mock_plugin_updates( $enabled ? WC()->version : '9.0.0' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up mock plugin updates.
|
||||
*
|
||||
* @param string $new_version The new version of WooCommerce to simulate.
|
||||
*/
|
||||
private function setup_mock_plugin_updates( $new_version ) {
|
||||
$update_plugins = (object) array(
|
||||
'response' => array(
|
||||
WC_PLUGIN_BASENAME => (object) array(
|
||||
'new_version' => $new_version,
|
||||
'package' => 'https://downloads.wordpress.org/plugin/woocommerce.zip',
|
||||
'slug' => 'woocommerce',
|
||||
),
|
||||
),
|
||||
);
|
||||
add_filter( 'pre_site_transient_update_plugins', fn() => $update_plugins );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
853
pnpm-lock.yaml
853
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue