Merge branch 'trunk' into update/woocommerce-blocks-10.4.0

This commit is contained in:
Tarun Vijwani 2023-06-16 12:27:35 +04:00 committed by GitHub
commit f896f268e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 2362 additions and 512 deletions

View File

@ -43,7 +43,7 @@ runs:
with:
php-version: ${{ inputs.php-version }}
coverage: none
tools: phpcs, sirbrillig/phpcs-changed
tools: phpcs, sirbrillig/phpcs-changed:2.10.2
- name: Cache Composer Dependencies
uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8

View File

@ -1,11 +1,18 @@
== Changelog ==
= 7.7.2 2023-06-01 =
**WooCommerce**
* Update - Update WooCommerce Blocks to 10.0.6 [#38533](https://github.com/woocommerce/woocommerce/pull/38533)
= 7.7.1 2023-05-26 =
**WooCommerce**
* Update - Update WooCommerce Blocks to 10.0.5 [#38427](https://github.com/woocommerce/woocommerce/pull/38427)
= 7.7.0 2023-05-10 =
**WooCommerce**

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Add extraFields and showDescription props

View File

@ -46,7 +46,8 @@ export const CustomerEffortScoreModalContainer: React.FC = () => {
const recordScore = (
score: number,
secondScore: number,
comments: string
comments: string,
extraFieldsValues: { [ key: string ]: string } = {}
) => {
recordEvent( 'ces_feedback', {
action: visibleCESModalData.action,
@ -54,6 +55,7 @@ export const CustomerEffortScoreModalContainer: React.FC = () => {
score_second_question: secondScore ?? null,
score_combined: score + ( secondScore ?? 0 ),
comments: comments || '',
...extraFieldsValues,
store_age: storeAgeInWeeks,
...visibleCESModalData.tracksProps,
} );
@ -75,6 +77,7 @@ export const CustomerEffortScoreModalContainer: React.FC = () => {
return (
<CustomerFeedbackModal
title={ visibleCESModalData.title }
showDescription={ visibleCESModalData.showDescription }
firstQuestion={ visibleCESModalData.firstQuestion }
secondQuestion={ visibleCESModalData.secondQuestion }
recordScoreCallback={ ( ...args ) => {
@ -87,6 +90,10 @@ export const CustomerEffortScoreModalContainer: React.FC = () => {
hideCesModal();
} }
shouldShowComments={ visibleCESModalData.props?.shouldShowComments }
getExtraFieldsToBeShown={
visibleCESModalData.getExtraFieldsToBeShown
}
validateExtraFields={ visibleCESModalData.validateExtraFields }
/>
);
};

View File

@ -21,6 +21,7 @@ type CustomerEffortScoreProps = {
) => void;
title?: string;
description?: string;
showDescription?: boolean;
noticeLabel?: string;
firstQuestion: string;
secondQuestion?: string;
@ -33,6 +34,14 @@ type CustomerEffortScoreProps = {
firstQuestionScore: number,
secondQuestionScore: number
) => boolean;
getExtraFieldsToBeShown?: (
extraFieldsValues: { [ key: string ]: string },
setExtraFieldsValues: ( values: { [ key: string ]: string } ) => void,
errors: Record< string, string > | undefined
) => JSX.Element;
validateExtraFields?: ( values: { [ key: string ]: string } ) => {
[ key: string ]: string;
};
};
/**
@ -45,6 +54,7 @@ type CustomerEffortScoreProps = {
* @param {Function} props.recordScoreCallback Function to call when the score should be recorded.
* @param {string} [props.title] The title displayed in the modal.
* @param {string} props.description The description displayed in the modal.
* @param {boolean} props.showDescription Show description in the modal.
* @param {string} props.noticeLabel The notice label displayed in the notice.
* @param {string} props.firstQuestion The first survey question.
* @param {string} [props.secondQuestion] The second survey question.
@ -54,11 +64,14 @@ type CustomerEffortScoreProps = {
* @param {Function} props.onModalDismissedCallback Function to call when modal is dismissed.
* @param {Function} props.shouldShowComments Callback to determine if comments section should be shown.
* @param {Object} props.icon Icon (React component) to be shown on the notice.
* @param {Function} props.getExtraFieldsToBeShown Function that returns the extra fields to be shown.
* @param {Function} props.validateExtraFields Function that validates the extra fields.
*/
const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( {
recordScoreCallback,
title,
description,
showDescription = true,
noticeLabel,
firstQuestion,
secondQuestion,
@ -71,6 +84,8 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( {
[ firstQuestionScore, secondQuestionScore ].some(
( score ) => score === 1 || score === 2
),
getExtraFieldsToBeShown,
validateExtraFields,
} ) => {
const [ shouldCreateNotice, setShouldCreateNotice ] = useState( true );
const [ visible, setVisible ] = useState( false );
@ -113,11 +128,14 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( {
<CustomerFeedbackModal
title={ title }
description={ description }
showDescription={ showDescription }
firstQuestion={ firstQuestion }
secondQuestion={ secondQuestion }
recordScoreCallback={ recordScoreCallback }
onCloseModal={ onModalDismissedCallback }
shouldShowComments={ shouldShowComments }
getExtraFieldsToBeShown={ getExtraFieldsToBeShown }
validateExtraFields={ validateExtraFields }
/>
);
};

View File

@ -23,21 +23,25 @@ import { __ } from '@wordpress/i18n';
*
* Upon completion, the score and comments is sent to a callback function.
*
* @param {Object} props Component props.
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
* @param {string} props.title Title displayed in the modal.
* @param {string} props.description Description displayed in the modal.
* @param {string} props.firstQuestion The first survey question.
* @param {string} [props.secondQuestion] An optional second survey question.
* @param {string} props.defaultScore Default score.
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
* @param {Function} props.customOptions List of custom score options, contains label and value.
* @param {Function} props.shouldShowComments A function to determine whether or not the comments field shown be shown.
* @param {Object} props Component props.
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
* @param {string} props.title Title displayed in the modal.
* @param {string} props.description Description displayed in the modal.
* @param {boolean} props.showDescription Show description in the modal.
* @param {string} props.firstQuestion The first survey question.
* @param {string} [props.secondQuestion] An optional second survey question.
* @param {string} props.defaultScore Default score.
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
* @param {Function} props.customOptions List of custom score options, contains label and value.
* @param {Function} props.shouldShowComments A function to determine whether or not the comments field shown be shown.
* @param {Function} props.getExtraFieldsToBeShown Function that returns the extra fields to be shown.
* @param {Function} props.validateExtraFields Function that validates the extra fields.
*/
function CustomerFeedbackModal( {
recordScoreCallback,
title = __( 'Please share your feedback', 'woocommerce' ),
description,
showDescription = true,
firstQuestion,
secondQuestion,
defaultScore = NaN,
@ -47,14 +51,18 @@ function CustomerFeedbackModal( {
[ firstQuestionScore, secondQuestionScore ].some(
( score ) => score === 1 || score === 2
),
getExtraFieldsToBeShown,
validateExtraFields,
}: {
recordScoreCallback: (
score: number,
secondScore: number,
comments: string
comments: string,
extraFieldsValues: { [ key: string ]: string }
) => void;
title?: string;
description?: string;
showDescription?: boolean;
firstQuestion: string;
secondQuestion?: string;
defaultScore?: number;
@ -64,6 +72,14 @@ function CustomerFeedbackModal( {
firstQuestionScore: number,
secondQuestionScore: number
) => boolean;
getExtraFieldsToBeShown?: (
extraFieldsValues: { [ key: string ]: string },
setExtraFieldsValues: ( values: { [ key: string ]: string } ) => void,
errors: Record< string, string > | undefined
) => JSX.Element;
validateExtraFields?: ( values: { [ key: string ]: string } ) => {
[ key: string ]: string;
};
} ): JSX.Element | null {
const options =
customOptions && customOptions.length > 0
@ -100,6 +116,12 @@ function CustomerFeedbackModal( {
const [ comments, setComments ] = useState( '' );
const [ showNoScoreMessage, setShowNoScoreMessage ] = useState( false );
const [ isOpen, setOpen ] = useState( true );
const [ extraFieldsValues, setExtraFieldsValues ] = useState< {
[ key: string ]: string;
} >( {} );
const [ errors, setErrors ] = useState<
Record< string, string > | undefined
>( {} );
const closeModal = () => {
setOpen( false );
@ -118,18 +140,27 @@ function CustomerFeedbackModal( {
};
const sendScore = () => {
if (
const missingFirstOrSecondQuestions =
! Number.isInteger( firstQuestionScore ) ||
( secondQuestion && ! Number.isInteger( secondQuestionScore ) )
) {
( secondQuestion && ! Number.isInteger( secondQuestionScore ) );
if ( missingFirstOrSecondQuestions ) {
setShowNoScoreMessage( true );
}
const extraFieldsErrors =
typeof validateExtraFields === 'function'
? validateExtraFields( extraFieldsValues )
: {};
const validExtraFields = Object.keys( extraFieldsErrors ).length === 0;
if ( missingFirstOrSecondQuestions || ! validExtraFields ) {
setErrors( extraFieldsErrors );
return;
}
setOpen( false );
recordScoreCallback(
firstQuestionScore,
secondQuestionScore,
comments
comments,
extraFieldsValues
);
};
@ -144,20 +175,22 @@ function CustomerFeedbackModal( {
onRequestClose={ closeModal }
shouldCloseOnClickOutside={ false }
>
<Text
variant="body"
as="p"
className="woocommerce-customer-effort-score__intro"
size={ 14 }
lineHeight="20px"
marginBottom="1.5em"
>
{ description ||
__(
'Your feedback will help create a better experience for thousands of merchants like you. Please tell us to what extent you agree or disagree with the statements below.',
'woocommerce'
) }
</Text>
{ showDescription && (
<Text
variant="body"
as="p"
className="woocommerce-customer-effort-score__intro"
size={ 14 }
lineHeight="20px"
marginBottom="1.5em"
>
{ description ||
__(
'Your feedback will help create a better experience for thousands of merchants like you. Please tell us to what extent you agree or disagree with the statements below.',
'woocommerce'
) }
</Text>
) }
<Text
variant="subtitle.small"
@ -244,13 +277,20 @@ function CustomerFeedbackModal( {
>
<Text variant="body" as="p">
{ __(
'Please provide feedback by selecting an option above.',
'Please tell us to what extent you agree or disagree with the statements above.',
'woocommerce'
) }
</Text>
</div>
) }
{ typeof getExtraFieldsToBeShown === 'function' &&
getExtraFieldsToBeShown(
extraFieldsValues,
setExtraFieldsValues,
errors
) }
<div className="woocommerce-customer-effort-score__buttons">
<Button isTertiary onClick={ closeModal } name="cancel">
{ __( 'Cancel', 'woocommerce' ) }
@ -270,6 +310,8 @@ CustomerFeedbackModal.propTypes = {
secondQuestion: PropTypes.string,
defaultScore: PropTypes.number,
onCloseModal: PropTypes.func,
getExtraFieldsToBeShown: PropTypes.func,
validateExtraFields: PropTypes.func,
};
export { CustomerFeedbackModal };

View File

@ -26,6 +26,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
case TYPES.SHOW_CES_MODAL:
const cesModalData = {
action: action.surveyProps.action,
showDescription: action.surveyProps.showDescription,
title: action.surveyProps.title,
onSubmitLabel: action.onSubmitLabel,
firstQuestion: action.surveyProps.firstQuestion,
@ -33,6 +34,9 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
onSubmitNoticeProps: action.onSubmitNoticeProps || {},
props: action.props,
tracksProps: action.tracksProps,
getExtraFieldsToBeShown:
action.surveyProps.getExtraFieldsToBeShown,
validateExtraFields: action.surveyProps.validateExtraFields,
};
return {
...state,

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add Jetpack Connection Auth endpoint.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Redirect users to WooCommerce Home when Jetpack auth endpoint returns an invalid URL

View File

@ -36,6 +36,7 @@ const TYPES = {
VISITED_TASK: 'VISITED_TASK',
KEEP_COMPLETED_TASKS_REQUEST: 'KEEP_COMPLETED_TASKS_REQUEST',
KEEP_COMPLETED_TASKS_SUCCESS: 'KEEP_COMPLETED_TASKS_SUCCESS',
SET_JETPACK_AUTH_URL: 'SET_JETPACK_AUTH_URL',
} as const;
export default TYPES;

View File

@ -18,6 +18,7 @@ import {
TaskType,
OnboardingProductTypes,
InstallAndActivatePluginsAsyncResponse,
GetJetpackAuthUrlResponse,
} from './types';
import { Plugin, PluginNames } from '../plugins/types';
@ -488,6 +489,19 @@ export function* installAndActivatePluginsAsync(
}
}
export function setJetpackAuthUrl(
results: GetJetpackAuthUrlResponse,
redirectUrl: string,
from = ''
) {
return {
type: TYPES.SET_JETPACK_AUTH_URL,
results,
redirectUrl,
from,
};
}
export type Action = ReturnType<
| typeof getFreeExtensionsError
| typeof getFreeExtensionsSuccess
@ -524,4 +538,5 @@ export type Action = ReturnType<
| typeof actionTaskRequest
| typeof getProductTypesError
| typeof getProductTypesSuccess
| typeof setJetpackAuthUrl
>;

View File

@ -38,6 +38,7 @@ export const defaultState: OnboardingState = {
productTypes: {},
requesting: {},
taskLists: {},
jetpackAuthUrls: {},
};
const getUpdatedTaskLists = (
@ -428,6 +429,14 @@ const reducer: Reducer< OnboardingState, Action > = (
},
taskLists: getUpdatedTaskLists( state.taskLists, action.task ),
};
case TYPES.SET_JETPACK_AUTH_URL:
return {
...state,
jetpackAuthUrls: {
...state.jetpackAuthUrls,
[ action.redirectUrl ]: action.results,
},
};
default:
return state;
}

View File

@ -20,10 +20,12 @@ import {
setEmailPrefill,
getProductTypesSuccess,
getProductTypesError,
setJetpackAuthUrl,
} from './actions';
import { DeprecatedTasks } from './deprecated-tasks';
import {
ExtensionList,
GetJetpackAuthUrlResponse,
OnboardingProductTypes,
ProfileItems,
TaskListType,
@ -136,3 +138,28 @@ export function* getProductTypes() {
yield getProductTypesError( error );
}
}
export function* getJetpackAuthUrl( query: {
redirectUrl: string;
from?: string;
} ) {
try {
let path =
WC_ADMIN_NAMESPACE +
'/onboarding/plugins/jetpack-authorization-url?redirect_url=' +
encodeURIComponent( query.redirectUrl );
if ( query.from ) {
path += '&from=' + query.from;
}
const results: GetJetpackAuthUrlResponse = yield apiFetch( {
path,
method: 'GET',
} );
yield setJetpackAuthUrl( results, query.redirectUrl, query.from ?? '' );
} catch ( error ) {
yield setError( 'getJetpackAuthUrl', error );
}
}

View File

@ -12,6 +12,7 @@ import {
OnboardingState,
ExtensionList,
ProfileItems,
GetJetpackAuthUrlResponse,
} from './types';
import { WPDataSelectors } from '../types';
import { Plugin } from '../plugins/types';
@ -95,6 +96,16 @@ export const getProductTypes = ( state: OnboardingState ) => {
return state.productTypes || {};
};
export const getJetpackAuthUrl = (
state: OnboardingState,
query: {
redirectUrl: string;
from?: string;
}
): GetJetpackAuthUrlResponse => {
return state.jetpackAuthUrls[ query.redirectUrl ] || '';
};
export type OnboardingSelectors = {
getProfileItems: () => ReturnType< typeof getProfileItems >;
getPaymentGatewaySuggestions: () => ReturnType<

View File

@ -57,6 +57,7 @@ describe( 'plugins reducer', () => {
emailPrefill: '',
errors: {},
requesting: {},
jetpackAuthUrls: {},
},
{
type: TYPES.SET_PROFILE_ITEMS,
@ -79,6 +80,7 @@ describe( 'plugins reducer', () => {
emailPrefill: '',
errors: {},
requesting: {},
jetpackAuthUrls: {},
},
{
type: TYPES.SET_PROFILE_ITEMS,

View File

@ -80,12 +80,19 @@ export type OnboardingState = {
// TODO clarify what the error record's type is
errors: Record< string, unknown >;
requesting: Record< string, boolean >;
jetpackAuthUrls: Record< string, GetJetpackAuthUrlResponse >;
};
export type Industry = {
slug: string;
};
export type GetJetpackAuthUrlResponse = {
url: string;
success: boolean;
errors: string[];
};
export type ProductCount = '0' | '1-10' | '11-100' | '101 - 1000' | '1000+';
export type ProductTypeSlug =
@ -191,7 +198,7 @@ export type Extension = {
export type InstallAndActivatePluginsAsyncResponse = {
job_id: string;
status: 'pending' | 'in-progress' | 'completed' | 'failed';
status: 'pendi<ng' | 'in-progress' | 'completed' | 'failed';
plugins: Array< {
status: 'pending' | 'installing' | 'installed' | 'activated' | 'failed';
errors: string[];

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add tool selector option to iframe editor

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix CES modal

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix empty state for currency inputs in product editor

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix overlapping TransientNotices with product editor footer

View File

@ -86,13 +86,17 @@ export function Editor( { product, settings }: EditorProps ) {
<PluginArea scope="woocommerce-product-block-editor" />
</>
}
footer={ <Footer product={ product } /> }
/>
<Popover.Slot />
</ValidationProvider>
</SlotFillProvider>
</ShortcutProvider>
{ /* We put Footer here instead of in InterfaceSkeleton because Footer uses
WooFooterItem to actually render in the WooFooterItem.Slot defined by
WooCommerce Admin. And, we need to put it outside of the SlotFillProvider
we create in this component. */ }
<Footer product={ product } />
</EntityProvider>
</StrictMode>
</LayoutContextProvider>

View File

@ -2,14 +2,18 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import {
BaseControl,
Button,
TextControl,
TextareaControl,
} from '@wordpress/components';
import {
createElement,
createInterpolateElement,
Fragment,
} from '@wordpress/element';
import { closeSmall } from '@wordpress/icons';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { Pill } from '@woocommerce/components';
import { useCustomerEffortScoreModal } from '@woocommerce/customer-effort-score';
import { Product } from '@woocommerce/data';
@ -20,6 +24,7 @@ import { recordEvent } from '@woocommerce/tracks';
*/
import { PRODUCT_EDITOR_FEEDBACK_CES_ACTION } from '../../constants';
import { useFeedbackBar } from '../../hooks/use-feedback-bar';
import { isValidEmail } from '../../utils';
export type FeedbackBarProps = {
product: Partial< Product >;
@ -46,8 +51,9 @@ export function FeedbackBar( { product }: FeedbackBarProps ) {
showCesModal(
{
action: PRODUCT_EDITOR_FEEDBACK_CES_ACTION,
showDescription: false,
title: __(
"How's your experience with the product editor?",
"How's your experience with the new product form?",
'woocommerce'
),
firstQuestion: __(
@ -55,14 +61,115 @@ export function FeedbackBar( { product }: FeedbackBarProps ) {
'woocommerce'
),
secondQuestion: __(
"The product editing screen's functionality meets my needs",
'Product form is easy to use',
'woocommerce'
),
onsubmitLabel: __(
"Thanks for the feedback. We'll put it to good use!",
'woocommerce'
),
shouldShowComments: () => true,
shouldShowComments: () => false,
getExtraFieldsToBeShown: (
values: {
email?: string;
additional_thoughts?: string;
},
setValues: ( value: {
email?: string;
additional_thoughts?: string;
} ) => void,
errors: Record< string, string > | undefined
) => (
<Fragment>
<BaseControl
id={ 'feedback_additional_thoughts' }
className="woocommerce-product-feedback__additional-thoughts"
label={ createInterpolateElement(
__(
'ADDITIONAL THOUGHTS <optional />',
'woocommerce'
),
{
optional: (
<span className="woocommerce-product-feedback__optional-input">
{ __(
'(OPTIONAL)',
'woocommerce'
) }
</span>
),
}
) }
>
<TextareaControl
value={ values.additional_thoughts || '' }
onChange={ ( value: string ) =>
setValues( {
...values,
additional_thoughts: value,
} )
}
help={ errors?.additional_thoughts || '' }
/>
</BaseControl>
<BaseControl
id={ 'feedback_email' }
className="woocommerce-product-feedback__email"
label={ createInterpolateElement(
__(
'YOUR EMAIL ADDRESS <optional />',
'woocommerce'
),
{
optional: (
<span className="woocommerce-product-feedback__optional-input">
{ __(
'(OPTIONAL)',
'woocommerce'
) }
</span>
),
}
) }
>
<TextControl
value={ values.email || '' }
onChange={ ( value: string ) =>
setValues( { ...values, email: value } )
}
help={ errors?.email || '' }
/>
<span>
{ __(
'In case you want to participate in further discussion and future user research.',
'woocommerce'
) }
</span>
</BaseControl>
</Fragment>
),
validateExtraFields: ( {
email = '',
additional_thoughts = '',
}: {
email?: string;
additional_thoughts?: string;
} ) => {
const errors: Record< string, string > | undefined = {};
if ( email.length > 0 && ! isValidEmail( email ) ) {
errors.email = __(
'Please enter a valid email address.',
'woocommerce'
);
}
if ( additional_thoughts?.length > 500 ) {
errors.additional_thoughts = __(
'Please enter no more than 500 characters.',
'woocommerce'
);
}
return errors;
},
},
{},
{
@ -93,42 +200,40 @@ export function FeedbackBar( { product }: FeedbackBarProps ) {
return (
<>
{ shouldShowFeedbackBar && (
<WooFooterItem>
<div className="woocommerce-product-mvp-ces-footer">
<Pill>Beta</Pill>
<div className="woocommerce-product-mvp-ces-footer__message">
{ createInterpolateElement(
__(
'How is your experience with the new product form? <span><shareButton>Share feedback</shareButton> or <turnOffButton>turn it off</turnOffButton></span>',
'woocommerce'
<div className="woocommerce-product-mvp-ces-footer">
<Pill>Beta</Pill>
<div className="woocommerce-product-mvp-ces-footer__message">
{ createInterpolateElement(
__(
'How is your experience with the new product form? <span><shareButton>Share feedback</shareButton> or <turnOffButton>turn it off</turnOffButton></span>',
'woocommerce'
),
{
span: (
<span className="woocommerce-product-mvp-ces-footer__message-buttons" />
),
{
span: (
<span className="woocommerce-product-mvp-ces-footer__message-buttons" />
),
shareButton: (
<Button
variant="link"
onClick={ onShareFeedbackClick }
/>
),
turnOffButton: (
<Button
onClick={ onTurnOffEditorClick }
variant="link"
/>
),
}
) }
</div>
<Button
className="woocommerce-product-mvp-ces-footer__close-button"
icon={ closeSmall }
label={ __( 'Hide this message', 'woocommerce' ) }
onClick={ onHideFeedbackBarClick }
></Button>
shareButton: (
<Button
variant="link"
onClick={ onShareFeedbackClick }
/>
),
turnOffButton: (
<Button
onClick={ onTurnOffEditorClick }
variant="link"
/>
),
}
) }
</div>
</WooFooterItem>
<Button
className="woocommerce-product-mvp-ces-footer__close-button"
icon={ closeSmall }
label={ __( 'Hide this message', 'woocommerce' ) }
onClick={ onHideFeedbackBarClick }
></Button>
</div>
) }
</>
);

View File

@ -1,3 +1,5 @@
$gutenberg-blue: var(--wp-admin-theme-color);
.woocommerce-product-mvp-ces-footer {
display: flex;
flex-direction: row;
@ -40,3 +42,59 @@
}
}
}
.components-modal__content {
> p {
text-transform: uppercase;
color: $gray-900;
}
> .components-base-control {
margin-top: $gap-small;
}
.woocommerce-customer-effort-score {
&__selection {
.components-radio-control__option {
margin-right: 0;
label {
color: $gray-900;
padding: 1em 0;
width: 8.8em;
height: 80px;
&::before {
margin: $gap-smaller 0;
}
&:hover {
box-shadow: 0 0 0 1.5px $gutenberg-blue;
border-radius: 2px;
background-color: #fff;
}
}
}
}
&__errors {
> p {
color: $error-red;
}
}
}
.woocommerce-product-feedback__additional-thoughts,
.woocommerce-product-feedback__email {
.components-base-control__help {
color: $error-red;
}
.components-base-control__label {
color: $gray-900;
line-height: 16px;
text-transform: uppercase;
.woocommerce-product-feedback__optional-input {
color: $gray-700;
}
}
.components-base-control__field {
> span {
font-size: 12px;
line-height: 16px;
color: $gray-700;
}
}
}
}

View File

@ -1,8 +1,7 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { createElement, Fragment } from '@wordpress/element';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { Product } from '@woocommerce/data';
@ -18,16 +17,11 @@ export type FooterProps = {
export function Footer( { product }: FooterProps ) {
return (
<div
className="woocommerce-product-footer"
role="region"
aria-label={ __( 'Product Editor bottom bar.', 'woocommerce' ) }
tabIndex={ -1 }
>
<WooFooterItem.Slot name="product" />
<FeedbackBar product={ product } />
<ProductMVPFeedbackModalContainer productId={ product.id } />
</div>
<WooFooterItem>
<>
<FeedbackBar product={ product } />
<ProductMVPFeedbackModalContainer productId={ product.id } />
</>
</WooFooterItem>
);
}

View File

@ -4,10 +4,6 @@
import { useSelect } from '@wordpress/data';
import { useViewportMatch } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import {
NavigableToolbar,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { plus } from '@wordpress/icons';
import {
createElement,
@ -17,6 +13,13 @@ import {
useContext,
} from '@wordpress/element';
import { MouseEvent } from 'react';
import {
NavigableToolbar,
store as blockEditorStore,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ToolSelector exists in WordPress components.
ToolSelector,
} from '@wordpress/block-editor';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ToolbarItem exists in WordPress components.
// eslint-disable-next-line @woocommerce/dependency-group
@ -33,8 +36,9 @@ export function HeaderToolbar() {
const { isInserterOpened, setIsInserterOpened } =
useContext( EditorContext );
const isWideViewport = useViewportMatch( 'wide' );
const isLargeViewport = useViewportMatch( 'medium' );
const inserterButton = useRef< HTMLButtonElement | null >( null );
const { isInserterEnabled } = useSelect( ( select ) => {
const { isInserterEnabled, isTextModeEnabled } = useSelect( ( select ) => {
const {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore These selectors are available in the block data store.
@ -45,9 +49,13 @@ export function HeaderToolbar() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore These selectors are available in the block data store.
getBlockSelectionEnd,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore These selectors are available in the block data store.
__unstableGetEditorMode: getEditorMode,
} = select( blockEditorStore );
return {
isTextModeEnabled: getEditorMode() === 'text',
isInserterEnabled: hasInserterItems(
getBlockRootClientId( getBlockSelectionEnd() )
),
@ -98,6 +106,12 @@ export function HeaderToolbar() {
/>
{ isWideViewport && (
<>
{ isLargeViewport && (
<ToolbarItem
as={ ToolSelector }
disabled={ isTextModeEnabled }
/>
) }
<ToolbarItem as={ EditorHistoryUndo } />
<ToolbarItem as={ EditorHistoryRedo } />
</>

View File

@ -81,7 +81,7 @@ export const useCurrencyInputProps = ( {
}
},
onChange( newValue: string ) {
const sanitizeValue = sanitizePrice( newValue || '0' );
const sanitizeValue = sanitizePrice( newValue );
if ( onChange ) {
onChange( sanitizeValue );
}

View File

@ -312,6 +312,10 @@ export function useProductHelper() {
*/
const sanitizePrice = useCallback(
( price: string ) => {
if ( ! price.length ) {
return '';
}
const { getCurrencyConfig } = context;
const { decimalSeparator } = getCurrencyConfig();
// Build regex to strip out everything except digits, decimal point and minus sign.

View File

@ -18,6 +18,7 @@ import {
getTruncatedProductVariationTitle,
} from './get-product-variation-title';
import { preventLeavingProductForm } from './prevent-leaving-product-form';
import { isValidEmail } from './validate-email';
export * from './create-ordered-children';
export * from './sort-fills-by-order';
@ -38,6 +39,7 @@ export {
getProductTitle,
getProductVariationTitle,
getTruncatedProductVariationTitle,
isValidEmail,
preventLeavingProductForm,
PRODUCT_STATUS_LABELS,
};

View File

@ -0,0 +1,11 @@
/**
* Checks if the provided email address is valid.
*
* @param {string} email - The email address to be tested.
* @return {boolean} Returns true if the email address is valid.
*/
export const isValidEmail = ( email: string ) => {
const re =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test( String( email ).toLowerCase() );
};

View File

@ -12,8 +12,15 @@ import {
UserProfileEvent,
BusinessInfoEvent,
PluginsLearnMoreLinkClicked,
PluginsInstallationCompletedWithErrorsEvent,
PluginsInstallationCompletedEvent,
} from '..';
import { POSSIBLY_DEFAULT_STORE_NAMES } from '../pages/BusinessInfo';
import {
InstalledPlugin,
PluginInstallError,
} from '../services/installAndActivatePlugins';
import { getPluginTrackKey, getTimeFrame } from '~/utils';
const recordTracksStepViewed = (
_context: unknown,
@ -21,7 +28,7 @@ const recordTracksStepViewed = (
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( 'storeprofiler_step_view', {
recordEvent( 'coreprofiler_step_view', {
step,
wc_version: getSetting( 'wcVersion' ),
} );
@ -33,12 +40,12 @@ const recordTracksStepSkipped = (
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( `storeprofiler_${ step }_skip` );
recordEvent( `coreprofiler_${ step }_skip` );
};
const recordTracksIntroCompleted = () => {
recordEvent( 'storeprofiler_step_complete', {
step: 'store_details',
recordEvent( 'coreprofiler_step_complete', {
step: 'intro_opt_in',
wc_version: getSetting( 'wcVersion' ),
} );
};
@ -47,12 +54,12 @@ const recordTracksUserProfileCompleted = (
_context: CoreProfilerStateMachineContext,
event: Extract< UserProfileEvent, { type: 'USER_PROFILE_COMPLETED' } >
) => {
recordEvent( 'storeprofiler_step_complete', {
recordEvent( 'coreprofiler_step_complete', {
step: 'user_profile',
wc_version: getSetting( 'wcVersion' ),
} );
recordEvent( 'storeprofiler_user_profile', {
recordEvent( 'coreprofiler_user_profile', {
business_choice: event.payload.userProfile.businessChoice,
selling_online_answer: event.payload.userProfile.sellingOnlineAnswer,
selling_platforms: event.payload.userProfile.sellingPlatforms
@ -62,7 +69,7 @@ const recordTracksUserProfileCompleted = (
};
const recordTracksSkipBusinessLocationCompleted = () => {
recordEvent( 'storeprofiler_step_complete', {
recordEvent( 'coreprofiler_step_complete', {
step: 'skip_business_location',
wc_version: getSetting( 'wcVersion' ),
} );
@ -72,12 +79,12 @@ const recordTracksBusinessInfoCompleted = (
_context: CoreProfilerStateMachineContext,
event: Extract< BusinessInfoEvent, { type: 'BUSINESS_INFO_COMPLETED' } >
) => {
recordEvent( 'storeprofiler_step_complete', {
recordEvent( 'coreprofiler_step_complete', {
step: 'business_info',
wc_version: getSetting( 'wcVersion' ),
} );
recordEvent( 'storeprofiler_business_info', {
recordEvent( 'coreprofiler_business_info', {
business_name_filled:
POSSIBLY_DEFAULT_STORE_NAMES.findIndex(
( name ) => name === event.payload.storeName
@ -96,12 +103,57 @@ const recordTracksPluginsLearnMoreLinkClicked = (
{ action }: { action: unknown }
) => {
const { step } = action as { step: string };
recordEvent( `storeprofiler_${ step }_learn_more_link_clicked`, {
recordEvent( `coreprofiler_${ step }_learn_more_link_clicked`, {
plugin: _event.payload.plugin,
link: _event.payload.learnMoreLink,
} );
};
const recordFailedPluginInstallations = (
_context: unknown,
_event: PluginsInstallationCompletedWithErrorsEvent
) => {
recordEvent( 'coreprofiler_store_extensions_installed_and_activated', {
success: false,
failed_extensions: _event.payload.errors.map(
( error: PluginInstallError ) => getPluginTrackKey( error.plugin )
),
} );
};
const recordSuccessfulPluginInstallation = (
_context: unknown,
_event: PluginsInstallationCompletedEvent
) => {
const installationCompletedResult =
_event.payload.installationCompletedResult;
const trackData: {
success: boolean;
installed_extensions: string[];
total_time: string;
[ key: string ]: number | boolean | string | string[];
} = {
success: true,
installed_extensions: installationCompletedResult.installedPlugins.map(
( installedPlugin: InstalledPlugin ) =>
getPluginTrackKey( installedPlugin.plugin )
),
total_time: getTimeFrame( installationCompletedResult.totalTime ),
};
for ( const installedPlugin of installationCompletedResult.installedPlugins ) {
trackData[
'install_time_' + getPluginTrackKey( installedPlugin.plugin )
] = getTimeFrame( installedPlugin.installTime );
}
recordEvent(
'coreprofiler_store_extensions_installed_and_activated',
trackData
);
};
export default {
recordTracksStepViewed,
recordTracksStepSkipped,
@ -110,4 +162,6 @@ export default {
recordTracksSkipBusinessLocationCompleted,
recordTracksBusinessInfoCompleted,
recordTracksPluginsLearnMoreLinkClicked,
recordFailedPluginInstallations,
recordSuccessfulPluginInstallation,
};

View File

@ -20,6 +20,10 @@
margin-top: 6px;
}
.components-checkbox-control__input-container {
margin-top: 6px;
}
input {
margin: 3px 26px 0 0;
width: 20px;

View File

@ -16,7 +16,12 @@ import {
import { useMachine, useSelector } from '@xstate/react';
import { useEffect, useMemo } from '@wordpress/element';
import { resolveSelect, dispatch } from '@wordpress/data';
import { updateQueryString, getQuery } from '@woocommerce/navigation';
import {
updateQueryString,
getQuery,
getNewPath,
navigateTo,
} from '@woocommerce/navigation';
import {
ExtensionList,
OPTIONS_STORE_NAME,
@ -25,10 +30,11 @@ import {
ONBOARDING_STORE_NAME,
Extension,
GeolocationResponse,
PLUGINS_STORE_NAME,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { initializeExPlat } from '@woocommerce/explat';
import { CountryStateOption } from '@woocommerce/onboarding';
import { getAdminLink } from '@woocommerce/settings';
/**
* Internal dependencies
@ -49,7 +55,7 @@ import { BusinessLocation } from './pages/BusinessLocation';
import { getCountryStateOptions } from './services/country';
import { Loader } from './pages/Loader';
import { Plugins } from './pages/Plugins';
import { getPluginSlug, getPluginTrackKey, getTimeFrame } from '~/utils';
import { getPluginSlug } from '~/utils';
import './style.scss';
import {
InstallationCompletedResult,
@ -174,6 +180,7 @@ export type CoreProfilerStateMachineContext = {
stageIndex?: number;
};
onboardingProfile: OnboardingProfile;
jetpackAuthUrl?: string;
};
const getAllowTrackingOption = async () =>
@ -320,10 +327,16 @@ const handleGeolocation = assign( {
} );
const redirectToWooHome = () => {
/**
* @todo replace with navigateTo
*/
window.location.href = '/wp-admin/admin.php?page=wc-admin';
navigateTo( {
url: getNewPath( {}, '/', {} ),
} );
};
const redirectToJetpackAuthPage = (
_context: CoreProfilerStateMachineContext,
event: { data: { url: string } }
) => {
window.location.href = event.data.url + '&installed_ext_success=1';
};
const updateTrackingOption = (
@ -364,6 +377,16 @@ const updateOnboardingProfileOption = (
} );
};
const spawnUpdateOnboardingProfileOption = assign( {
spawnUpdateOnboardingProfileOptionRef: (
context: CoreProfilerStateMachineContext
) =>
spawn(
() => updateOnboardingProfileOption( context ),
'update-onboarding-profile'
),
} );
const updateBusinessLocation = ( countryAndState: string ) => {
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
woocommerce_default_country: countryAndState,
@ -389,7 +412,7 @@ const assignUserProfile = assign( {
const updateBusinessInfo = async (
_context: CoreProfilerStateMachineContext,
event: BusinessInfoEvent
event: AnyEventObject
) => {
const refreshedOnboardingProfile = ( await resolveSelect(
OPTIONS_STORE_NAME
@ -518,7 +541,9 @@ const coreProfilerMachineActions = {
handleOnboardingProfileOption,
assignOnboardingProfile,
persistBusinessInfo,
spawnUpdateOnboardingProfileOption,
redirectToWooHome,
redirectToJetpackAuthPage,
};
const coreProfilerMachineServices = {
@ -530,6 +555,7 @@ const coreProfilerMachineServices = {
getOnboardingProfileOption,
getPlugins,
browserPopstateHandler,
updateBusinessInfo,
};
export const coreProfilerStateMachineDefinition = createMachine( {
id: 'coreProfiler',
@ -561,6 +587,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
pluginsSelected: [],
loader: {},
onboardingProfile: {} as OnboardingProfile,
jetpackAuthUrl: undefined,
} as CoreProfilerStateMachineContext,
states: {
navigate: {
@ -681,7 +708,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
entry: [
{
type: 'recordTracksStepViewed',
step: 'store_details',
step: 'intro_opt_in',
},
{ type: 'updateQueryStep', step: 'intro-opt-in' },
],
@ -697,7 +724,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
actions: [
{
type: 'recordTracksStepSkipped',
step: 'store_details',
step: 'intro_opt_in',
},
],
},
@ -772,16 +799,9 @@ export const coreProfilerStateMachineDefinition = createMachine( {
] ),
},
postUserProfile: {
invoke: {
src: ( context ) => {
return updateOnboardingProfileOption( context );
},
onDone: {
target: '#businessInfo',
},
onError: {
target: '#businessInfo',
},
entry: [ 'spawnUpdateOnboardingProfileOption' ],
always: {
target: '#businessInfo',
},
},
},
@ -938,11 +958,19 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
on: {
BUSINESS_INFO_COMPLETED: {
target: 'postBusinessInfo',
actions: [ 'recordTracksBusinessInfoCompleted' ],
},
},
},
postBusinessInfo: {
invoke: {
src: 'updateBusinessInfo',
onDone: {
target: '#plugins',
},
onError: {
target: '#plugins',
actions: [
'persistBusinessInfo',
'recordTracksBusinessInfoCompleted',
],
},
},
},
@ -999,9 +1027,20 @@ export const coreProfilerStateMachineDefinition = createMachine( {
} ),
invoke: {
src: ( context ) => {
return updateBusinessLocation(
context.businessInfo.location as string
);
const skipped = dispatch(
ONBOARDING_STORE_NAME
).updateProfileItems( {
skipped: true,
} );
const businessLocation =
updateBusinessLocation(
context.businessInfo
.location as string
);
return Promise.all( [
skipped,
businessLocation,
] );
},
onDone: {
target: 'progress20',
@ -1147,8 +1186,65 @@ export const coreProfilerStateMachineDefinition = createMachine( {
completed: true,
} );
},
onDone: [
{
target: 'isJetpackConnected',
cond: 'hasJetpackSelected',
},
{ actions: 'redirectToWooHome' },
],
},
meta: {
component: Loader,
progress: 100,
},
},
isJetpackConnected: {
invoke: {
src: async () => {
return await resolveSelect(
PLUGINS_STORE_NAME
).isJetpackConnected();
},
onDone: [
{
target: 'sendToJetpackAuthPage',
cond: ( _context, event ) => {
return ! event.data;
},
},
{ actions: 'redirectToWooHome' },
],
},
meta: {
component: Loader,
progress: 100,
},
},
sendToJetpackAuthPage: {
invoke: {
src: async () =>
await resolveSelect(
ONBOARDING_STORE_NAME
).getJetpackAuthUrl( {
redirectUrl: getAdminLink(
'admin.php?page=wc-admin'
),
from: 'woocommerce-core-profiler',
} ),
onDone: {
actions: 'redirectToWooHome',
actions: actions.choose( [
{
cond: ( _context, event ) =>
event.data.success === true,
actions: 'redirectToJetpackAuthPage',
},
{
cond: ( _context, event ) =>
event.data.success === false,
actions: 'redirectToWooHome',
},
] ),
},
},
meta: {
@ -1198,73 +1294,16 @@ export const coreProfilerStateMachineDefinition = createMachine( {
event
) => event.payload.errors,
} ),
( _context, event ) => {
recordEvent(
'storeprofiler_store_extensions_installed_and_activated',
{
success: false,
failed_extensions:
event.payload.errors.map(
(
error: PluginInstallError
) =>
getPluginTrackKey(
error.plugin
)
),
}
);
{
type: 'recordFailedPluginInstallations',
},
],
},
PLUGINS_INSTALLATION_COMPLETED: {
target: 'postPluginInstallation',
actions: [
( _context, event ) => {
const installationCompletedResult =
event.payload
.installationCompletedResult;
const trackData: {
success: boolean;
installed_extensions: string[];
total_time: string;
[ key: string ]:
| number
| boolean
| string
| string[];
} = {
success: true,
installed_extensions:
installationCompletedResult.installedPlugins.map(
(
installedPlugin: InstalledPlugin
) =>
getPluginTrackKey(
installedPlugin.plugin
)
),
total_time: getTimeFrame(
installationCompletedResult.totalTime
),
};
for ( const installedPlugin of installationCompletedResult.installedPlugins ) {
trackData[
'install_time_' +
getPluginTrackKey(
installedPlugin.plugin
)
] = getTimeFrame(
installedPlugin.installTime
);
}
recordEvent(
'storeprofiler_store_extensions_installed_and_activated',
trackData
);
{
type: 'recordSuccessfulPluginInstallation',
},
],
},
@ -1323,6 +1362,17 @@ export const CoreProfilerController = ( {
step === ( cond as { step: string | undefined } ).step
);
},
hasJetpackSelected: ( context ) => {
return (
context.pluginsSelected.find(
( plugin ) => plugin === 'jetpack'
) !== undefined ||
context.pluginsAvailable.find(
( plugin: Extension ) =>
plugin.key === 'jetpack' && plugin.is_activated
) !== undefined
);
},
},
} );
}, [ actionOverrides, servicesOverrides ] );
@ -1337,8 +1387,8 @@ export const CoreProfilerController = ( {
);
const navigationProgress = currentNodeMeta?.progress;
const CurrentComponent =
currentNodeMeta?.component ?? ( () => <ProfileSpinner /> ); // If no component is defined for the state then its a loading state
const CurrentComponent = currentNodeMeta?.component;
const currentNodeCssLabel =
state.value instanceof Object
@ -1364,13 +1414,15 @@ export const CoreProfilerController = ( {
<div
className={ `woocommerce-profile-wizard__container woocommerce-profile-wizard__step-${ currentNodeCssLabel }` }
>
{
{ CurrentComponent ? (
<CurrentComponent
navigationProgress={ navigationProgress }
sendEvent={ send }
context={ state.context }
/>
}
) : (
<ProfileSpinner />
) }
</div>
</>
);

View File

@ -91,6 +91,7 @@ export const BusinessInfo = ( {
onboardingProfile: {
is_store_country_set: isStoreCountrySet,
industry: industryFromOnboardingProfile,
business_choice: businessChoiceFromOnboardingProfile,
},
} = context;
@ -152,7 +153,9 @@ export const BusinessInfo = ( {
const selectCountryLabel = __( 'Select country/region', 'woocommerce' );
const selectIndustryQuestionLabel =
selectIndustryMapping[
businessChoice || 'im_just_starting_my_business'
businessChoice ||
businessChoiceFromOnboardingProfile ||
'im_just_starting_my_business'
];
const [ dismissedGeolocationNotice, setDismissedGeolocationNotice ] =

View File

@ -1,12 +1,16 @@
.woocommerce-layout__footer {
background: $studio-white;
border-top: 1px solid $gray-200;
box-sizing: border-box;
padding: 0;
position: fixed;
width: calc(100% - 160px);
bottom: 0;
z-index: 1001;
/* on top of #wp-content-editor-tools */
bottom: -1px; /* to hide the border when no visible children */
z-index: 1001; /* on top of #wp-content-editor-tools */
.woocommerce-profile-wizard__body & {
width: 100%;
}
@include breakpoint('782px-960px') {
width: calc(100% - 36px);

View File

@ -43,7 +43,7 @@ import { Controller, getPages } from './controller';
import { Header } from '../header';
import { Footer } from './footer';
import Notices from './notices';
import TransientNotices from './transient-notices';
import { TransientNotices } from './transient-notices';
import { getAdminSetting } from '~/utils/admin-settings';
import { usePageClasses } from './hooks/use-page-classes';
import '~/activity-panel';

View File

@ -3,6 +3,7 @@
*/
import { applyFilters } from '@wordpress/hooks';
import classnames from 'classnames';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { OPTIONS_STORE_NAME, USER_STORE_NAME } from '@woocommerce/data';
import PropTypes from 'prop-types';
import { useDispatch, useSelect } from '@wordpress/data';
@ -17,7 +18,7 @@ import './style.scss';
const QUEUE_OPTION = 'woocommerce_admin_transient_notices_queue';
const QUEUED_NOTICE_FILTER = 'woocommerce_admin_queued_notice_filter';
function TransientNotices( props ) {
export function TransientNotices( props ) {
const { removeNotice: onRemove } = useDispatch( 'core/notices' );
const { createNotice: createNotice2, removeNotice: onRemove2 } =
useDispatch( 'core/notices2' );
@ -89,12 +90,14 @@ function TransientNotices( props ) {
const combinedNotices = getNotices();
return (
<SnackbarList
notices={ combinedNotices }
className={ classes }
onRemove={ onRemove }
onRemove2={ onRemove2 }
/>
<WooFooterItem>
<SnackbarList
notices={ combinedNotices }
className={ classes }
onRemove={ onRemove }
onRemove2={ onRemove2 }
/>
</WooFooterItem>
);
}
@ -108,5 +111,3 @@ TransientNotices.propTypes = {
*/
notices: PropTypes.array,
};
export default TransientNotices;

View File

@ -1,32 +1,22 @@
@import '../../navigation/stylesheets/variables.scss';
.woocommerce-transient-notices {
position: fixed;
bottom: $gap-small;
left: $admin-menu-width + $gap;
position: absolute;
left: $gap;
bottom: 100%;
margin-bottom: $gap-small;
z-index: calc(z-index('.components-snackbar-list') + 1);
width: auto;
@media ( max-width: 960px ) {
left: 50px;
}
@media ( max-width: 782px ) {
left: $gap;
}
.woocommerce-profile-wizard__body & {
left: unset;
width: 100%;
.components-snackbar {
margin-left: auto;
margin-right: auto;
}
}
.has-woocommerce-navigation & {
left: $navigation-width + $gap;
}
}
.components-snackbar {

View File

@ -7,7 +7,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import TransientNotices from '..';
import { TransientNotices } from '..';
jest.mock( '@wordpress/data', () => {
// Require the original module to not be mocked...
@ -26,6 +26,18 @@ useDispatch.mockReturnValue( {
createNotice: jest.fn(),
} );
jest.mock( '@woocommerce/admin-layout', () => {
const originalModule = jest.requireActual( '@woocommerce/admin-layout' );
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
WooFooterItem: jest.fn( ( { children } ) => {
return <div>{ children }</div>;
} ),
};
} );
jest.mock( '../snackbar/list', () =>
jest.fn( ( { notices } ) => {
return notices.map( ( notice ) => (

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { MenuItem } from '@wordpress/components';
import { info, Icon } from '@wordpress/icons';
import { useState } from '@wordpress/element';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import BlockEditorGuide from '~/products/tour/block-editor/block-editor-guide';
import { usePublishedProductsCount } from '~/products/tour/block-editor/use-published-products-count';
export const AboutTheEditorMenuItem = ( {
onClose,
}: {
onClose: () => void;
} ) => {
const [ isGuideOpen, setIsGuideOpen ] = useState( false );
const { isNewUser } = usePublishedProductsCount();
return (
<>
<MenuItem
onClick={ () => {
recordEvent(
'block_product_editor_about_the_editor_menu_item_clicked'
);
setIsGuideOpen( true );
} }
icon={ <Icon icon={ info } /> }
iconPosition="right"
>
{ __( 'About the editor…', 'woocommerce' ) }
</MenuItem>
{ isGuideOpen && (
<BlockEditorGuide
isNewUser={ isNewUser }
onCloseGuide={ () => {
setIsGuideOpen( false );
onClose();
} }
/>
) }
</>
);
};

View File

@ -1,20 +1,20 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { getAdminLink } from '@woocommerce/settings';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { OPTIONS_STORE_NAME, Product } from '@woocommerce/data';
import { MenuItem } from '@wordpress/components';
import {
ALLOW_TRACKING_OPTION_NAME,
STORE_KEY as CES_STORE_KEY,
} from '@woocommerce/customer-effort-score';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { ClassicEditorIcon } from '../../images/classic-editor-icon';
import { getAdminSetting } from '~/utils/admin-settings';
export const ClassicEditorMenuItem = ( {
@ -53,24 +53,46 @@ export const ClassicEditorMenuItem = ( {
`post-new.php?post_type=product&product_block_editor=0&_feature_nonce=${ _feature_nonce }`
);
const { type: productType, status: productStatus } = useSelect(
( select ) => {
const { getEntityRecord } = select( 'core' );
return getEntityRecord(
'postType',
'product',
productId
) as Product;
},
[ productId ]
);
if ( isLoading ) {
return null;
}
function handleMenuItemClick() {
recordEvent( 'product_editor_options_turn_off_editor_click', {
product_id: productId,
product_type: productType,
product_status: productStatus,
} );
if ( allowTracking ) {
showProductMVPFeedbackModal();
} else {
window.location.href = classicEditorUrl;
}
onClose();
}
return (
<MenuItem
onClick={ () => {
if ( allowTracking ) {
showProductMVPFeedbackModal();
} else {
window.location.href = classicEditorUrl;
}
onClose();
} }
icon={ <ClassicEditorIcon /> }
iconPosition="right"
onClick={ handleMenuItemClick }
info={ __(
'Save changes and go back to the classic product editing screen.',
'woocommerce'
) }
>
{ __( 'Use the classic editor', 'woocommerce' ) }
{ __( 'Turn off the new product form', 'woocommerce' ) }
</MenuItem>
);
};

View File

@ -2,10 +2,21 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { MenuItem } from '@wordpress/components';
import {
BaseControl,
MenuItem,
TextControl,
TextareaControl,
} from '@wordpress/components';
import {
createElement,
createInterpolateElement,
Fragment,
} from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
import { useLayoutContext } from '@woocommerce/admin-layout';
import { isValidEmail } from '@woocommerce/product-editor';
/**
* Internal dependencies
@ -22,8 +33,9 @@ export const FeedbackMenuItem = ( { onClose }: { onClose: () => void } ) => {
showCesModal(
{
action: 'new_product',
showDescription: false,
title: __(
"How's your experience with the product editor?",
"How's your experience with the new product form?",
'woocommerce'
),
firstQuestion: __(
@ -34,9 +46,118 @@ export const FeedbackMenuItem = ( { onClose }: { onClose: () => void } ) => {
"The product editing screen's functionality meets my needs",
'woocommerce'
),
getExtraFieldsToBeShown: (
values: {
email?: string;
additional_thoughts?: string;
},
setValues: ( value: {
email?: string;
additional_thoughts?: string;
} ) => void,
errors: Record< string, string > | undefined
) => (
<Fragment>
<BaseControl
id={ 'feedback_additional_thoughts' }
className="woocommerce-product-feedback__additional-thoughts"
label={ createInterpolateElement(
__(
'ADDITIONAL THOUGHTS <optional />',
'woocommerce'
),
{
optional: (
<span className="woocommerce-product-feedback__optional-input">
{ __(
'(OPTIONAL)',
'woocommerce'
) }
</span>
),
}
) }
>
<TextareaControl
value={
values.additional_thoughts || ''
}
onChange={ ( value: string ) =>
setValues( {
...values,
additional_thoughts: value,
} )
}
help={
errors?.additional_thoughts || ''
}
/>
</BaseControl>
<BaseControl
id={ 'feedback_email' }
className="woocommerce-product-feedback__email"
label={ createInterpolateElement(
__(
'YOUR EMAIL ADDRESS <optional />',
'woocommerce'
),
{
optional: (
<span className="woocommerce-product-feedback__optional-input">
{ __(
'(OPTIONAL)',
'woocommerce'
) }
</span>
),
}
) }
>
<TextControl
value={ values.email || '' }
onChange={ ( value: string ) =>
setValues( {
...values,
email: value,
} )
}
help={ errors?.email || '' }
/>
<span>
{ __(
'In case you want to participate in further discussion and future user research.',
'woocommerce'
) }
</span>
</BaseControl>
</Fragment>
),
validateExtraFields: ( {
email = '',
additional_thoughts = '',
}: {
email?: string;
additional_thoughts?: string;
} ) => {
const errors: Record< string, string > | undefined =
{};
if ( email.length > 0 && ! isValidEmail( email ) ) {
errors.email = __(
'Please enter a valid email address.',
'woocommerce'
);
}
if ( additional_thoughts?.length > 500 ) {
errors.additional_thoughts = __(
'Please enter no more than 500 characters.',
'woocommerce'
);
}
return errors;
},
},
{
shouldShowComments: () => true,
shouldShowComments: () => false,
},
{
type: 'snackbar',

View File

@ -1,2 +1,3 @@
export * from './feedback-menu-item';
export * from './classic-editor-menu-item';
export * from './about-the-editor-menu-item';

View File

@ -19,6 +19,7 @@ import { useEntityProp } from '@wordpress/core-data';
import {
FeedbackMenuItem,
ClassicEditorMenuItem,
AboutTheEditorMenuItem,
} from '../fills/more-menu-items';
const MoreMenuFill = ( { onClose }: { onClose: () => void } ) => {
@ -28,6 +29,7 @@ const MoreMenuFill = ( { onClose }: { onClose: () => void } ) => {
<>
<FeedbackMenuItem onClose={ onClose } />
<ClassicEditorMenuItem productId={ id } onClose={ onClose } />
<AboutTheEditorMenuItem onClose={ onClose } />
</>
);
};

View File

@ -10,11 +10,95 @@ import { __ } from '@wordpress/i18n';
import Guide from '../components/guide';
import './style.scss';
interface Props {
const PageContent = ( {
page,
}: {
page: {
heading: string;
text: string;
};
} ) => (
<>
<h1 className="woocommerce-block-editor-guide__heading">
{ page.heading }
</h1>
<p className="woocommerce-block-editor-guide__text">{ page.text }</p>
</>
);
const PageImage = ( {
page,
}: {
page: {
index: number;
};
} ) => (
<div
className={ `woocommerce-block-editor-guide__header woocommerce-block-editor-guide__header-${
page.index + 1
}` }
></div>
);
interface BlockEditorGuideProps {
isNewUser: boolean;
onCloseGuide: ( currentPage: number, origin: 'close' | 'finish' ) => void;
}
const BlockEditorGuide = ( { onCloseGuide }: Props ) => {
const BlockEditorGuide = ( {
isNewUser,
onCloseGuide,
}: BlockEditorGuideProps ) => {
const pagesConfig = [
{
heading: isNewUser
? __( 'Fresh and modern interface', 'woocommerce' )
: __( 'Refreshed, streamlined interface', 'woocommerce' ),
text: isNewUser
? __(
'Using the product form means less clicking around. Product details are neatly grouped by tabs, so you always know where to go.',
'woocommerce'
)
: __(
'Experience a simpler, more focused interface with a modern design that enhances usability.',
'woocommerce'
),
},
{
heading: __( 'Content-rich product descriptions', 'woocommerce' ),
text: __(
'Create compelling product pages with blocks, media, images, videos, and any content you desire to engage customers.',
'woocommerce'
),
},
{
heading: isNewUser
? __( 'Speed & performance', 'woocommerce' )
: __( 'Improved speed & performance', 'woocommerce' ),
text: isNewUser
? __(
'Create a product from start to finish without page reloads. Our modern technology ensures reliability and lightning-fast performance.',
'woocommerce'
)
: __(
'Enjoy a seamless experience without page reloads. Our modern technology ensures reliability and lightning-fast performance.',
'woocommerce'
),
},
{
heading: __( 'More features are on the way', 'woocommerce' ),
text: __(
'While we currently support physical products, exciting updates are coming to accommodate more types, like digital products, variations, and more. Stay tuned!',
'woocommerce'
),
},
];
const pages = pagesConfig.map( ( page, index ) => ( {
content: <PageContent page={ page } />,
image: <PageImage page={ { ...page, index } } />,
} ) );
return (
<Guide
className="woocommerce-block-editor-guide"
@ -22,92 +106,7 @@ const BlockEditorGuide = ( { onCloseGuide }: Props ) => {
finishButtonText={ __( 'Tell me more', 'woocommerce' ) }
finishButtonLink="https://woocommerce.com/product-form-beta"
onFinish={ onCloseGuide }
pages={ [
{
content: (
<>
<h1 className="woocommerce-block-editor-guide__heading">
{ __(
'Refreshed, streamlined interface',
'woocommerce'
) }
</h1>
<p className="woocommerce-block-editor-guide__text">
{ __(
'Experience a simpler, more focused interface with a modern design that enhances usability.',
'woocommerce'
) }
</p>
</>
),
image: (
<div className="woocommerce-block-editor-guide__background1"></div>
),
},
{
content: (
<>
<h1 className="woocommerce-block-editor-guide__heading">
{ __(
'Content-rich product descriptions',
'woocommerce'
) }
</h1>
<p className="woocommerce-block-editor-guide__text">
{ __(
'Create compelling product pages with blocks, media, images, videos, and any content you desire to engage customers.',
'woocommerce'
) }
</p>
</>
),
image: (
<div className="woocommerce-block-editor-guide__background2"></div>
),
},
{
content: (
<>
<h1 className="woocommerce-block-editor-guide__heading">
{ __(
'Improved speed & performance',
'woocommerce'
) }
</h1>
<p className="woocommerce-block-editor-guide__text">
{ __(
'Enjoy a seamless experience without page reloads. Our modern technology ensures reliability and lightning-fast performance.',
'woocommerce'
) }
</p>
</>
),
image: (
<div className="woocommerce-block-editor-guide__background3"></div>
),
},
{
content: (
<>
<h1 className="woocommerce-block-editor-guide__heading">
{ __(
'More features are on the way',
'woocommerce'
) }
</h1>
<p className="woocommerce-block-editor-guide__text">
{ __(
'While we currently support physical products, exciting updates are coming to accommodate more types, like digital products, variations, and more. Stay tuned!',
'woocommerce'
) }
</p>
</>
),
image: (
<div className="woocommerce-block-editor-guide__background4"></div>
),
},
] }
pages={ pages }
/>
);
};

View File

@ -1,11 +1,11 @@
/**
* External dependencies
*/
import { Pill, TourKit } from '@woocommerce/components';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Pill, TourKit } from '@woocommerce/components';
import { __experimentalUseFeedbackBar as useFeedbackBar } from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
@ -13,6 +13,7 @@ import { __experimentalUseFeedbackBar as useFeedbackBar } from '@woocommerce/pro
import './style.scss';
import BlockEditorGuide from './block-editor-guide';
import { usePublishedProductsCount } from './use-published-products-count';
interface Props {
shouldTourBeShown: boolean;
@ -20,6 +21,9 @@ interface Props {
}
const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
const { isNewUser, loadingPublishedProductsCount } =
usePublishedProductsCount();
useEffect( () => {
if ( shouldTourBeShown ) {
recordEvent( 'block_product_editor_spotlight_view' );
@ -34,9 +38,31 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
setIsGuideOpen( true );
};
const getTourText = () => {
return {
heading: isNewUser
? __( 'Meet the product editing form', 'woocommerce' )
: __( 'A new way to edit your products', 'woocommerce' ),
description: isNewUser
? __(
"Discover the form's unique features designed to help you make this product stand out.",
'woocommerce'
)
: __(
'Introducing the upgraded experience designed to help you create and edit products easier.',
'woocommerce'
),
};
};
if ( loadingPublishedProductsCount ) {
return null;
}
if ( isGuideOpen ) {
return (
<BlockEditorGuide
isNewUser={ isNewUser }
onCloseGuide={ ( currentPage, source ) => {
dismissModal();
if ( source === 'finish' ) {
@ -58,6 +84,8 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
/>
);
} else if ( shouldTourBeShown ) {
const { heading, description } = getTourText();
return (
<TourKit
config={ {
@ -72,20 +100,12 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
),
},
descriptions: {
desktop: __(
"We designed a brand new product editing experience to let you focus on what's important.",
'woocommerce'
),
desktop: description,
},
heading: (
<>
<span>
{ __(
'Meet a streamlined product form',
'woocommerce'
) }
</span>{ ' ' }
<Pill className="woocommerce-block-editor-guide__pill">
<span>{ heading }</span>
<Pill>
{ __( 'Beta', 'woocommerce' ) }
</Pill>
</>

View File

@ -0,0 +1,31 @@
<svg width="312" height="222" viewBox="0 0 312 222" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="312" height="222" fill="#F6F7F7"/>
<path d="M87.6168 96L92 96L92 135L92 174L87.6168 174C87.6168 156.805 82.1737 139.763 77.5075 139.763C66.1856 139.763 60.3578 146.362 52.0988 146.362C47.2305 146.362 40 144.478 40 135C40 125.527 47.2305 123.638 52.0988 123.638C60.3578 123.638 66.1856 130.237 77.5075 130.237C82.1737 130.237 87.6168 113.195 87.6168 96ZM45.7754 135C45.7754 137.175 47.5763 138.94 49.7949 138.94C52.0135 138.94 53.8144 137.175 53.8144 135C53.8144 132.825 52.0135 131.06 49.7949 131.06C47.5763 131.06 45.7754 132.825 45.7754 135Z" fill="#FF6BBB"/>
<path d="M91.8418 96L174.842 96L174.842 135L174.842 174L91.8418 174L91.8418 96Z" fill="#966CCF"/>
<g style="mix-blend-mode:soft-light">
<path d="M175 174L153.339 174C128.943 171.352 104 165.176 104 157.441C104 145.4 131.103 147.776 131.103 139.435C131.103 131.093 117.909 135.078 117.909 125.438C117.909 115.797 152.495 113 174.984 113L174.984 174L175 174Z" fill="white"/>
</g>
<rect x="193" y="49" width="51" height="51" rx="25.5" fill="#FFC350"/>
<rect x="193" y="49" width="51" height="51" rx="25.5" fill="url(#paint0_linear_1997_17544)"/>
<path d="M219.619 84.0169L217.458 78.7849H206.589L204.428 84.0169H203.348L211.471 64.5217H212.58L220.703 84.0169H219.622H219.619ZM212.024 65.6027L206.938 77.9082H217.106L212.02 65.6027H212.024Z" fill="white"/>
<path d="M232.944 84.0169V82.2918C231.627 83.7247 230.051 84.3657 228.065 84.3657C225.581 84.3657 223.127 82.6688 223.127 79.6585C223.127 76.6481 225.552 74.9512 228.065 74.9512C230.054 74.9512 231.631 75.5954 232.944 77.0252V73.8388C232.944 71.5575 231.131 70.3319 228.97 70.3319C227.129 70.3319 225.757 70.9761 224.44 72.5536L223.768 71.9691C225.141 70.3602 226.602 69.5432 228.97 69.5432C231.747 69.5432 233.82 70.9478 233.82 73.8105V84.0106H232.944V84.0169ZM232.944 81.415V77.9082C231.863 76.4753 230.139 75.7462 228.298 75.7462C225.725 75.7462 224.092 77.4714 224.092 79.6616C224.092 81.8518 225.728 83.577 228.298 83.577C230.139 83.577 231.863 82.848 232.944 81.415Z" fill="white"/>
<g style="mix-blend-mode:multiply">
<rect x="92" y="49" width="83" height="28" rx="14" fill="#007CBA"/>
<rect x="92" y="49" width="83" height="28" rx="14" fill="url(#paint1_linear_1997_17544)" fill-opacity="0.4"/>
<path d="M160.655 70.3093C164.882 70.3093 168.309 66.8822 168.309 62.6547C168.309 58.4271 164.882 55 160.655 55C156.427 55 153 58.4271 153 62.6547C153 66.8822 156.427 70.3093 160.655 70.3093Z" fill="white"/>
</g>
<circle cx="218" cy="167" r="6.44257" transform="rotate(-90 218 167)" stroke="#BBBBBB" stroke-width="1.11486"/>
<circle cx="218" cy="121" r="6.44257" transform="rotate(-90 218 121)" stroke="#BBBBBB" stroke-width="1.11486"/>
<ellipse cx="218" cy="144" rx="7" ry="7" transform="rotate(-90 218 144)" fill="#007CBA"/>
<circle cx="218" cy="144" r="10.4426" transform="rotate(-90 218 144)" stroke="#BBBBBB" stroke-width="1.11486"/>
<defs>
<linearGradient id="paint0_linear_1997_17544" x1="207.845" y1="52.0186" x2="224.58" y2="97.7763" gradientUnits="userSpaceOnUse">
<stop stop-color="#FBA500"/>
<stop offset="1" stop-color="#FBA500" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_1997_17544" x1="94" y1="55" x2="116.529" y2="74.1452" gradientUnits="userSpaceOnUse">
<stop stop-color="#2508D3"/>
<stop offset="1" stop-color="#2508D3" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,69 @@
<svg width="381" height="194" viewBox="0 0 381 194" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1865_198367)">
<rect width="381" height="194.31" fill="#D4AAF6"/>
<g clip-path="url(#clip1_1865_198367)">
<rect width="272.832" height="167.123" transform="translate(54.576 27.3745)" fill="white"/>
<g clip-path="url(#clip2_1865_198367)">
<rect x="128.162" y="113.562" width="51.2842" height="5.92333" rx="1.79505" fill="#E0E0E0"/>
<rect x="128.417" y="89.7881" width="74.9435" height="3.59011" rx="1.79505" fill="#E0E0E0"/>
<rect x="142.521" y="123.863" width="35.2835" height="3.38476" rx="1.69238" fill="#E0E0E0"/>
<rect x="128.417" y="96.0708" width="94.2403" height="3.59011" rx="1.79505" fill="#E0E0E0"/>
<rect x="142.521" y="131.459" width="44.3096" height="3.38476" rx="1.69238" fill="#E0E0E0"/>
<rect x="128.417" y="102.354" width="33.2085" height="3.59011" rx="1.79505" fill="#E0E0E0"/>
<rect x="142.521" y="139.055" width="35.2835" height="3.38476" rx="1.69238" fill="#E0E0E0"/>
<ellipse cx="136.506" cy="125.662" rx="1.35834" ry="1.39935" fill="#007CBA"/>
<rect x="128.412" y="122.913" width="10.7545" height="5.49692" rx="2.74846" stroke="#BBBBBB" stroke-width="0.500309"/>
<ellipse cx="131.46" cy="133.258" rx="1.35834" ry="1.39935" fill="#CCCCCC"/>
<rect x="128.412" y="130.51" width="10.7545" height="5.49692" rx="2.74846" stroke="#BBBBBB" stroke-width="0.500309"/>
<ellipse cx="136.506" cy="140.855" rx="1.35834" ry="1.39935" fill="#007CBA"/>
<rect x="128.412" y="138.106" width="10.7545" height="5.49691" rx="2.74846" stroke="#BBBBBB" stroke-width="0.500309"/>
</g>
<rect x="128.048" y="74.2329" width="51.2842" height="5.92333" rx="1.79505" fill="#E0E0E0"/>
<g clip-path="url(#clip3_1865_198367)">
<rect x="128.347" y="155.979" width="37.2573" height="14.7931" rx="1.79505" fill="#E0E0E0"/>
<rect opacity="0.6" x="169.097" y="155.979" width="37.2573" height="14.7931" rx="1.79505" fill="#E0E0E0"/>
<rect opacity="0.6" x="209.847" y="155.979" width="37.2573" height="14.7931" rx="1.79505" fill="#E0E0E0"/>
<rect x="128.414" y="177.969" width="51.2842" height="5.92333" rx="1.79505" fill="#E0E0E0"/>
<rect x="128.414" y="188.124" width="35.2835" height="3.38476" rx="1.69238" fill="#E0E0E0"/>
</g>
<g clip-path="url(#clip4_1865_198367)">
<mask id="path-20-inside-1_1865_198367" fill="white">
<path d="M54.3712 27.113H327.668V57.1802H54.3712V27.113Z"/>
</mask>
<path d="M54.3712 27.113H327.668V57.1802H54.3712V27.113Z" fill="white"/>
<rect x="160.123" y="34.312" width="60.9476" height="4.65829" rx="1.79505" fill="#E0E0E0"/>
<rect x="288.488" y="31.7712" width="33.3761" height="9.74006" rx="1.79505" fill="#007CBA"/>
<rect x="258.981" y="34.312" width="24.6693" height="4.65829" rx="1.79505" fill="#E0E0E0"/>
<rect x="125.732" y="48.4114" width="21.2833" height="3.38785" rx="1.69392" fill="#E0E0E0"/>
<rect x="125.732" y="55.6106" width="21.2833" height="1.69392" fill="#007CBA"/>
<rect x="179.908" y="48.4114" width="21.2833" height="3.38785" rx="1.69392" fill="#E0E0E0"/>
<rect x="152.82" y="48.4114" width="21.2833" height="3.38785" rx="1.69392" fill="#E0E0E0"/>
<rect x="206.996" y="48.4114" width="21.2833" height="3.38785" rx="1.69392" fill="#E0E0E0"/>
<rect x="234.083" y="48.4114" width="21.2833" height="3.38785" rx="1.69392" fill="#E0E0E0"/>
<rect width="17.9505" height="17.9505" transform="translate(54.127 26.9258)" fill="#23282D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.6536 35.7083C67.6536 33.0946 65.5234 30.9648 62.9093 30.9648C60.2904 30.9648 58.1649 33.0946 58.1649 35.7083C58.1649 38.3266 60.2904 40.4517 62.9093 40.4517C65.5234 40.4517 67.6536 38.3266 67.6536 35.7083ZM61.8561 38.2555L60.2382 33.9153C60.4992 33.9058 60.7933 33.8773 60.7933 33.8773C61.0305 33.8489 61.0021 33.3413 60.7649 33.3508C60.7649 33.3508 60.0769 33.403 59.6404 33.403C59.555 33.403 59.4649 33.403 59.3653 33.3983C60.1196 32.2409 61.4243 31.4914 62.9093 31.4914C64.0147 31.4914 65.0205 31.9041 65.7796 32.6014C65.457 32.5492 64.9968 32.7864 64.9968 33.3508C64.9968 33.6582 65.1605 33.9219 65.3446 34.2184L65.3446 34.2184L65.3446 34.2184C65.3707 34.2605 65.3973 34.3033 65.4238 34.3469C65.5899 34.6363 65.6847 34.992 65.6847 35.5138C65.6847 36.2206 65.0205 37.8855 65.0205 37.8855L63.583 33.9153C63.8392 33.9058 63.972 33.8346 63.972 33.8346C64.2092 33.8109 64.1808 33.2417 63.9436 33.256C63.9436 33.256 63.2604 33.3129 62.8144 33.3129C62.4017 33.3129 61.709 33.256 61.709 33.256C61.4718 33.2417 61.4433 33.8252 61.6805 33.8346L62.117 33.8726L62.7148 35.4901L61.8561 38.2555ZM66.4354 35.6807L66.425 35.7082C66.0812 36.6132 65.74 37.5259 65.3995 38.4368C65.2795 38.7578 65.1596 39.0785 65.0397 39.3986C66.3064 38.6681 67.1272 37.2688 67.1272 35.7082C67.1272 34.9777 66.9611 34.3042 66.629 33.6923C66.7718 34.7879 66.5525 35.3698 66.4354 35.6807ZM61.0591 39.5457C59.6453 38.8627 58.6917 37.3827 58.6917 35.7083C58.6917 35.0916 58.8008 34.5319 59.0333 34.0054L59.4596 35.1735C59.9914 36.6307 60.5238 38.0897 61.0591 39.5457ZM64.1951 39.7118L62.9711 36.4008C62.7456 37.066 62.5184 37.7312 62.2905 38.3983C62.1349 38.8539 61.9789 39.3105 61.823 39.7687C62.1645 39.873 62.5346 39.9252 62.9094 39.9252C63.3601 39.9252 63.7871 39.8493 64.1951 39.7118Z" fill="white"/>
</g>
<path d="M327.668 56.507H54.3712V57.8533H327.668V56.507Z" fill="#F0F0F0" mask="url(#path-20-inside-1_1865_198367)"/>
</g>
<path d="M335.943 125.677C340.391 121.466 339.702 105.926 339.702 104.113H340.487C340.487 105.926 339.801 121.466 344.246 125.677C347.527 128.784 349.836 129.969 363.309 129.969V130.678C349.836 130.678 347.527 131.863 344.246 134.97C339.797 139.182 340.487 154.721 340.487 156.534H339.702C339.702 154.721 340.388 139.182 335.943 134.97C332.661 131.863 330.352 130.678 316.879 130.678V129.969C330.352 129.969 332.661 128.784 335.943 125.677Z" fill="#F4C759"/>
<path opacity="0.5" d="M296.273 118.445C298.179 116.642 297.884 109.99 297.884 109.213H298.22C298.22 109.99 297.926 116.642 299.831 118.445C301.236 119.775 302.226 120.283 307.998 120.283V120.586C302.226 120.586 301.236 121.093 299.831 122.424C297.925 124.226 298.22 130.879 298.22 131.655H297.884C297.884 130.879 298.178 124.226 296.273 122.424C294.867 121.093 293.878 120.586 288.106 120.586V120.283C293.878 120.283 294.867 119.775 296.273 118.445Z" fill="#F4C759"/>
<path opacity="0.7" d="M304.496 158.655C306.927 156.353 306.55 147.862 306.55 146.871H306.979C306.979 147.862 306.604 156.353 309.033 158.655C310.826 160.353 312.088 161 319.45 161V161.387C312.088 161.387 310.826 162.035 309.033 163.733C306.602 166.034 306.979 174.526 306.979 175.516H306.55C306.55 174.526 306.925 166.034 304.496 163.733C302.703 162.035 301.441 161.387 294.079 161.387V161C301.441 161 302.703 160.353 304.496 158.655Z" fill="#F4C759"/>
</g>
<defs>
<clipPath id="clip0_1865_198367">
<rect width="381" height="194.31" fill="white"/>
</clipPath>
<clipPath id="clip1_1865_198367">
<rect width="272.832" height="167.123" fill="white" transform="translate(54.576 27.3745)"/>
</clipPath>
<clipPath id="clip2_1865_198367">
<rect width="126.102" height="61.4806" fill="white" transform="translate(127.968 89.7881)"/>
</clipPath>
<clipPath id="clip3_1865_198367">
<rect width="119.371" height="48.9152" fill="white" transform="translate(128.173 155.721)"/>
</clipPath>
<clipPath id="clip4_1865_198367">
<path d="M54.3712 27.113H327.668V57.1802H54.3712V27.113Z" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -1,30 +1,29 @@
$background-height: 220px;
$yellow: #f5e6ab;
$light-purple: #f2edff;
.woocommerce-block-editor-guide {
&__background1 {
height: $background-height;
background-color: $light-purple;
}
&__background2 {
height: $background-height;
background-color: #dfd1fb;
}
&__background3 {
height: $background-height;
background-color: #cfb9f6;
}
&__background4 {
height: $background-height;
background-color: #bea0f2;
}
&__pill {
border: 1px solid $yellow;
background-color: $yellow;
&__header {
width: 312px;
height: 222px;
background-color: #f6f7f7; /* WP Gray 0; no var available */
background-size: cover;
&-1 {
background-image: url(./images/guide-1.svg);
}
&-2 {
background-image: url(./images/guide-2.png);
}
&-3 {
background-image: url(./images/guide-3.png);
}
&-4 {
background-image: url(./images/guide-4.png);
}
}
&.components-modal__frame {
max-width: 320px;
max-width: 312px;
}
&__heading,
&__text {
@ -59,10 +58,26 @@ $light-purple: #f2edff;
}
.woocommerce-block-editor-tourkit {
.components-card__header {
align-items: flex-start;
height: 200px;
background-color: $light-purple;
margin-bottom: $gap;
.woocommerce-tour-kit-step {
width: 381px;
.components-card__header {
align-items: flex-start;
height: 194px;
background-color: #d4aaf6; /* no var available */
background-image: url(./images/tour-header.svg);
border-bottom: 1px solid $gray-200;
margin-bottom: $gap;
}
&__heading {
.woocommerce-pill {
margin-left: $gap-small;
background-color: $studio-yellow-5;
border: 0;
}
}
}
}

View File

@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { PRODUCTS_STORE_NAME } from '@woocommerce/data';
import { useSelect } from '@wordpress/data';
const PUBLISHED_PRODUCTS_QUERY_PARAMS = {
status: 'publish',
_fields: [ 'id' ],
};
export const usePublishedProductsCount = () => {
return useSelect( ( select ) => {
const { getProductsTotalCount, hasFinishedResolution } =
select( PRODUCTS_STORE_NAME );
const publishedProductsCount = getProductsTotalCount(
PUBLISHED_PRODUCTS_QUERY_PARAMS,
0
) as number;
const loadingPublishedProductsCount = ! hasFinishedResolution(
'getProductsTotalCount',
[ PUBLISHED_PRODUCTS_QUERY_PARAMS, 0 ]
);
return {
publishedProductsCount,
loadingPublishedProductsCount,
// we consider a user new if they have no published products
isNewUser: publishedProductsCount < 1,
};
} );
};

View File

@ -2,11 +2,10 @@
* External dependencies
*/
import { useDispatch } from '@wordpress/data';
import { ITEMS_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
import { getAdminLink } from '@woocommerce/settings';
import { loadExperimentAssignment } from '@woocommerce/explat';
import moment from 'moment';
import { useState } from '@wordpress/element';
/**
@ -14,13 +13,11 @@ import { useState } from '@wordpress/element';
*/
import { ProductTypeKey } from './constants';
import { createNoticesFromResponse } from '../../../lib/notices';
const NEW_PRODUCT_MANAGEMENT = 'woocommerce_new_product_management_enabled';
import { getAdminSetting } from '~/utils/admin-settings';
export const useCreateProductByType = () => {
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );
const [ isRequesting, setIsRequesting ] = useState< boolean >( false );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const isNewExperienceEnabled =
window.wcAdminFeatures[ 'new-product-management-experience' ];
@ -35,23 +32,19 @@ export const useCreateProductByType = () => {
setIsRequesting( true );
if ( type === 'physical' ) {
const momentDate = moment().utc();
const year = momentDate.format( 'YYYY' );
const month = momentDate.format( 'MM' );
const assignment = await loadExperimentAssignment(
`woocommerce_product_creation_experience_${ year }${ month }_v1`
);
if ( isNewExperienceEnabled ) {
navigateTo( { url: getNewPath( {}, '/add-product', {} ) } );
return;
}
const assignment = await loadExperimentAssignment(
'woocommerce_product_creation_experience_202306_v2'
);
if ( assignment.variationName === 'treatment' ) {
await updateOptions( {
[ NEW_PRODUCT_MANAGEMENT ]: 'yes',
} );
const _feature_nonce = getAdminSetting( '_feature_nonce' );
window.location.href = getAdminLink(
'admin.php?page=wc-admin&path=/add-product'
`post-new.php?post_type=product&product_block_editor=1&_feature_nonce=${ _feature_nonce }`
);
return;
}

View File

@ -240,7 +240,7 @@
position: absolute;
z-index: 0;
right: 6%;
right: 24px;
max-width: 25%;
max-height: 150px;
width: auto;
@ -264,7 +264,7 @@
max-width: 380px;
}
max-width: 75%;
max-width: 70%;
p,
span {
color: $gray-600;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Replace 'use classic editor' with 'Turn off the new product editor' in options menu#38575

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Implement the product blocks experiment within code for new users

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Product Editor Onboarding: Add About the editor... option the more menu in product block editor

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Update status only when it's changed.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Fix number of orders under tax report

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
CES modal: styling fixes and extraFiels prop added

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Add support for taxonomy meta boxes in HPOS order edit screen.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fixed a visual bug where text overlapped the image in the task list header.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Add re-migrate support to HPOS CLI.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: fix
Redirect users to WooCommerce Home when Jetpack auth endpoint returns an invalid URL.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Fixed race condition in core profiler's plugin list fetching and also minor spinner fixes

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Consider if user is new or not when clicking in "About the editor"

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
add HPOSToggleTrait.php to unit test loader

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix overlapping TransientNotices with footer

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Adds info about features and plugin compatibility to the data collected by WC Tracker

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Use coreprofiler_ prefix for core profiler track names

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove StoreDetails task when core-profiler flag is on

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update Action Scheduler to 3.6.1.

View File

@ -0,0 +1,5 @@
Significance: patch
Type: enhancement
Comment: Add wcadmin_settings_change tracks event when adding/removing entries in shipping

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add Jetpack Connection package

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Update product editor tour/guide copy and style.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Set woocommerce_onboarding_profile.skipped when guided set is skipped

View File

@ -15,12 +15,14 @@
],
"require": {
"php": ">=7.3",
"automattic/jetpack-autoloader": "2.10.1",
"automattic/jetpack-constants": "1.5.1",
"automattic/jetpack-autoloader": "2.11.18",
"automattic/jetpack-config": "1.15.2",
"automattic/jetpack-connection": "1.51.7",
"automattic/jetpack-constants": "^1.6.22",
"composer/installers": "^1.9",
"maxmind-db/reader": "^1.11",
"pelago/emogrifier": "^6.0",
"woocommerce/action-scheduler": "3.5.4",
"woocommerce/action-scheduler": "3.6.1",
"woocommerce/woocommerce-blocks": "10.4.2"
},
"require-dev": {

View File

@ -7,35 +7,136 @@
"content-hash": "79382fd9f5521821b18242e0214c91a1",
"packages": [
{
"name": "automattic/jetpack-autoloader",
"version": "2.10.1",
"name": "automattic/jetpack-a8c-mc-stats",
"version": "v1.4.20",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
"reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3"
"url": "https://github.com/Automattic/jetpack-a8c-mc-stats.git",
"reference": "6743d34fe7556455e17cbe1b7c90ed39a1f69089"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/20393c4677765c3e737dcb5aee7a3f7b90dce4b3",
"reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3",
"url": "https://api.github.com/repos/Automattic/jetpack-a8c-mc-stats/zipball/6743d34fe7556455e17cbe1b7c90ed39a1f69089",
"reference": "6743d34fe7556455e17cbe1b7c90ed39a1f69089",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-a8c-mc-stats",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-a8c-mc-stats/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Used to record internal usage stats for Automattic. Not visible to site owners.",
"support": {
"source": "https://github.com/Automattic/jetpack-a8c-mc-stats/tree/v1.4.20"
},
"time": "2023-04-10T11:43:38+00:00"
},
{
"name": "automattic/jetpack-admin-ui",
"version": "v0.2.20",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-admin-ui.git",
"reference": "90f4de6c9d936bbf161f1c2356d98b00ba33576f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-admin-ui/zipball/90f4de6c9d936bbf161f1c2356d98b00ba33576f",
"reference": "90f4de6c9d936bbf161f1c2356d98b00ba33576f",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"automattic/jetpack-logo": "^1.6.1",
"automattic/wordbless": "dev-master",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-admin-ui",
"textdomain": "jetpack-admin-ui",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-admin-ui/compare/${old}...${new}"
},
"branch-alias": {
"dev-trunk": "0.2.x-dev"
},
"version-constants": {
"::PACKAGE_VERSION": "src/class-admin-menu.php"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Generic Jetpack wp-admin UI elements",
"support": {
"source": "https://github.com/Automattic/jetpack-admin-ui/tree/v0.2.20"
},
"time": "2023-04-25T15:05:53+00:00"
},
{
"name": "automattic/jetpack-autoloader",
"version": "v2.11.18",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
"reference": "53cbf0528fa6931c4fa6465bccd37514f9eda720"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/53cbf0528fa6931c4fa6465bccd37514f9eda720",
"reference": "53cbf0528fa6931c4fa6465bccd37514f9eda720",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1 || ^2.0"
},
"require-dev": {
"automattic/jetpack-changelogger": "^1.1",
"yoast/phpunit-polyfills": "0.2.0"
"automattic/jetpack-changelogger": "^3.3.2",
"yoast/phpunit-polyfills": "1.0.4"
},
"type": "composer-plugin",
"extra": {
"autotagger": true,
"class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin",
"mirror-repo": "Automattic/jetpack-autoloader",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-master": "2.10.x-dev"
"dev-trunk": "2.11.x-dev"
}
},
"autoload": {
@ -52,29 +153,153 @@
],
"description": "Creates a custom autoloader for a plugin or theme.",
"support": {
"source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1"
"source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.11.18"
},
"time": "2021-03-30T15:15:59+00:00"
"time": "2023-03-29T12:51:59+00:00"
},
{
"name": "automattic/jetpack-constants",
"version": "v1.5.1",
"name": "automattic/jetpack-config",
"version": "v1.15.2",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-constants.git",
"reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b"
"url": "https://github.com/Automattic/jetpack-config.git",
"reference": "f1fa6e24a89192336a1499968bf8c68e173b6e34"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/18f772daddc8be5df76c9f4a92e017a3c2569a5b",
"reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b",
"url": "https://api.github.com/repos/Automattic/jetpack-config/zipball/f1fa6e24a89192336a1499968bf8c68e173b6e34",
"reference": "f1fa6e24a89192336a1499968bf8c68e173b6e34",
"shasum": ""
},
"require-dev": {
"php-mock/php-mock": "^2.1",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5"
"automattic/jetpack-changelogger": "^3.3.2"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-config",
"textdomain": "jetpack-config",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.15.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.",
"support": {
"source": "https://github.com/Automattic/jetpack-config/tree/v1.15.2"
},
"time": "2023-04-10T11:43:31+00:00"
},
{
"name": "automattic/jetpack-connection",
"version": "v1.51.7",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-connection.git",
"reference": "4c4bae836858957d9aaf6854cf4e24c3261242c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-connection/zipball/4c4bae836858957d9aaf6854cf4e24c3261242c4",
"reference": "4c4bae836858957d9aaf6854cf4e24c3261242c4",
"shasum": ""
},
"require": {
"automattic/jetpack-a8c-mc-stats": "^1.4.20",
"automattic/jetpack-admin-ui": "^0.2.19",
"automattic/jetpack-constants": "^1.6.22",
"automattic/jetpack-redirect": "^1.7.25",
"automattic/jetpack-roles": "^1.4.23",
"automattic/jetpack-status": "^1.16.4"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"automattic/wordbless": "@dev",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-connection",
"textdomain": "jetpack-connection",
"version-constants": {
"::PACKAGE_VERSION": "src/class-package-version.php"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-connection/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.51.x-dev"
}
},
"autoload": {
"classmap": [
"legacy",
"src/",
"src/webhooks"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Everything needed to connect to the Jetpack infrastructure",
"support": {
"source": "https://github.com/Automattic/jetpack-connection/tree/v1.51.7"
},
"time": "2023-04-10T11:44:13+00:00"
},
{
"name": "automattic/jetpack-constants",
"version": "v1.6.22",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-constants.git",
"reference": "7b5c44d763c7b0dd7498be2b41a89bfefe84834c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/7b5c44d763c7b0dd7498be2b41a89bfefe84834c",
"reference": "7b5c44d763c7b0dd7498be2b41a89bfefe84834c",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-constants",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-constants/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.6.x-dev"
}
},
"type": "library",
"autoload": {
"classmap": [
"src/"
@ -86,9 +311,160 @@
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
"source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1"
"source": "https://github.com/Automattic/jetpack-constants/tree/v1.6.22"
},
"time": "2020-10-28T19:00:31+00:00"
"time": "2023-04-10T11:43:45+00:00"
},
{
"name": "automattic/jetpack-redirect",
"version": "v1.7.25",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-redirect.git",
"reference": "67d7dce123d4af4fec4b4fe15e99aaad85308314"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-redirect/zipball/67d7dce123d4af4fec4b4fe15e99aaad85308314",
"reference": "67d7dce123d4af4fec4b4fe15e99aaad85308314",
"shasum": ""
},
"require": {
"automattic/jetpack-status": "^1.16.4"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-redirect",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-redirect/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.7.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Utilities to build URLs to the jetpack.com/redirect/ service",
"support": {
"source": "https://github.com/Automattic/jetpack-redirect/tree/v1.7.25"
},
"time": "2023-04-10T11:44:05+00:00"
},
{
"name": "automattic/jetpack-roles",
"version": "v1.4.23",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-roles.git",
"reference": "f147b3e8061fc0de2a892ddc4f4156eb995545f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-roles/zipball/f147b3e8061fc0de2a892ddc4f4156eb995545f9",
"reference": "f147b3e8061fc0de2a892ddc4f4156eb995545f9",
"shasum": ""
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-roles",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-roles/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.4.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Utilities, related with user roles and capabilities.",
"support": {
"source": "https://github.com/Automattic/jetpack-roles/tree/v1.4.23"
},
"time": "2023-04-10T11:43:48+00:00"
},
{
"name": "automattic/jetpack-status",
"version": "v1.17.1",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-status.git",
"reference": "0032ee4bce1d4644722ba46858c702a0afa76cff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-status/zipball/0032ee4bce1d4644722ba46858c702a0afa76cff",
"reference": "0032ee4bce1d4644722ba46858c702a0afa76cff",
"shasum": ""
},
"require": {
"automattic/jetpack-constants": "^1.6.22"
},
"require-dev": {
"automattic/jetpack-changelogger": "^3.3.2",
"automattic/jetpack-ip": "^0.1.3",
"brain/monkey": "2.6.1",
"yoast/phpunit-polyfills": "1.0.4"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
},
"type": "jetpack-library",
"extra": {
"autotagger": true,
"mirror-repo": "Automattic/jetpack-status",
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-status/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "1.17.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Used to retrieve information about the current status of Jetpack and the site overall.",
"support": {
"source": "https://github.com/Automattic/jetpack-status/tree/v1.17.1"
},
"time": "2023-05-11T05:50:45+00:00"
},
{
"name": "composer/installers",
@ -588,16 +964,16 @@
},
{
"name": "woocommerce/action-scheduler",
"version": "3.5.4",
"version": "3.6.1",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/action-scheduler.git",
"reference": "9533e71b0eba4a519721dde84a34dfb161f11eb8"
"reference": "7fd383cad3d64b419ec81bcd05bab44355a6e6ef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/9533e71b0eba4a519721dde84a34dfb161f11eb8",
"reference": "9533e71b0eba4a519721dde84a34dfb161f11eb8",
"url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/7fd383cad3d64b419ec81bcd05bab44355a6e6ef",
"reference": "7fd383cad3d64b419ec81bcd05bab44355a6e6ef",
"shasum": ""
},
"require-dev": {
@ -622,9 +998,9 @@
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
"source": "https://github.com/woocommerce/action-scheduler/tree/3.5.4"
"source": "https://github.com/woocommerce/action-scheduler/tree/3.6.1"
},
"time": "2023-01-17T20:20:43+00:00"
"time": "2023-06-14T19:23:12+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",

View File

@ -158,22 +158,28 @@ class WC_Report_Taxes_By_Code extends WC_Admin_Report {
// Merge.
$tax_rows = array();
// Initialize an associative array to store unique post_ids.
$unique_post_ids = array();
foreach ( $tax_rows_orders + $tax_rows_partial_refunds as $tax_row ) {
$key = $tax_row->rate_id;
$key = $tax_row->tax_rate;
$tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array(
'tax_amount' => 0,
'shipping_tax_amount' => 0,
'total_orders' => 0,
);
$tax_rows[ $key ]->total_orders += 1;
$tax_rows[ $key ]->tax_rate = $tax_row->tax_rate;
$tax_rows[ $key ]->tax_amount += wc_round_tax_total( $tax_row->tax_amount );
$tax_rows[ $key ]->shipping_tax_amount += wc_round_tax_total( $tax_row->shipping_tax_amount );
if ( ! isset( $unique_post_ids[ $key ] ) || ! in_array( $tax_row->post_id, $unique_post_ids[ $key ], true ) ) {
$unique_post_ids[ $key ] = isset( $unique_post_ids[ $key ] ) ? $unique_post_ids[ $key ] : array();
$unique_post_ids[ $key ][] = $tax_row->post_id;
$tax_rows[ $key ]->total_orders += 1;
}
}
foreach ( $tax_rows_full_refunds as $tax_row ) {
$key = $tax_row->rate_id;
$key = $tax_row->tax_rate;
$tax_rows[ $key ] = isset( $tax_rows[ $key ] ) ? $tax_rows[ $key ] : (object) array(
'tax_amount' => 0,
'shipping_tax_amount' => 0,

View File

@ -2995,6 +2995,18 @@ class WC_AJAX {
// That's fine, it's not in the database anyways. NEXT!
continue;
}
/**
* Notify that a non-option setting has been deleted.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'shipping_zone',
'action' => 'delete',
)
);
WC_Shipping_Zones::delete_zone( $zone_id );
continue;
}
@ -3024,19 +3036,18 @@ class WC_AJAX {
);
$zone->set_zone_order( $zone_data['zone_order'] );
}
global $current_tab;
$current_tab = 'shipping';
/**
* Completes the saving process for options.
*
* @since 7.8.0
*/
do_action( 'woocommerce_update_options' );
$zone->save();
}
}
global $current_tab;
$current_tab = 'shipping';
/**
* Completes the saving process for options.
*
* @since 7.8.0
*/
do_action( 'woocommerce_update_options' );
wp_send_json_success(
array(
'zones' => WC_Shipping_Zones::get_zones( 'json' ),
@ -3066,15 +3077,31 @@ class WC_AJAX {
$zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
$zone = new WC_Shipping_Zone( $zone_id );
// A shipping zone can be created here if the user is adding a method without first saving the shipping zone.
if ( '' === $zone_id ) {
/**
* Notified that a non-option setting has been added.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'shipping_zone',
'action' => 'add',
)
);
}
/**
* Notify that a non-option setting has been updated.
* Notify that a non-option setting has been added.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'zone_method',
'id' => 'zone_method',
'action' => 'add',
)
);
$instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) );
@ -3178,11 +3205,26 @@ class WC_AJAX {
$zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
$zone = new WC_Shipping_Zone( $zone_id );
// A shipping zone can be created here if the user is adding a method without first saving the shipping zone.
if ( '' === $zone_id ) {
/**
* Notifies that a non-option setting has been added.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'shipping_zone',
'action' => 'add',
)
);
}
$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( isset( $changes['zone_name'] ) ) {
/**
* Completes the saving process for options.
* Notifies that a non-option setting has been updated.
*
* @since 7.8.0
*/
@ -3192,7 +3234,7 @@ class WC_AJAX {
if ( isset( $changes['zone_locations'] ) ) {
/**
* Completes the saving process for options.
* Notifies that a non-option setting has been updated.
*
* @since 7.8.0
*/
@ -3218,7 +3260,7 @@ class WC_AJAX {
if ( isset( $changes['zone_postcodes'] ) ) {
/**
* Completes the saving process for options.
* Notifies that a non-option setting has been updated.
*
* @since 7.8.0
*/
@ -3231,12 +3273,6 @@ class WC_AJAX {
}
if ( isset( $changes['methods'] ) ) {
/**
* Completes the saving process for options.
*
* @since 7.8.0
*/
do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'zone_methods' ) );
foreach ( $changes['methods'] as $instance_id => $data ) {
$method_id = $wpdb->get_var( $wpdb->prepare( "SELECT method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d", $instance_id ) );
@ -3245,6 +3281,18 @@ class WC_AJAX {
$option_key = $shipping_method->get_instance_option_key();
if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) {
delete_option( $option_key );
/**
* Notifies that a non-option setting has been deleted.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'zone_method',
'action' => 'delete',
)
);
do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id );
}
continue;
@ -3260,7 +3308,7 @@ class WC_AJAX {
if ( isset( $method_data['method_order'] ) ) {
/**
* Completes the saving process for options.
* Notifies that a non-option setting has been updated.
*
* @since 7.8.0
*/
@ -3270,7 +3318,7 @@ class WC_AJAX {
if ( isset( $method_data['enabled'] ) ) {
/**
* Completes the saving process for options.
* Notifies that a non-option setting has been updated.
*
* @since 7.8.0
*/
@ -3385,6 +3433,18 @@ class WC_AJAX {
// That's fine, it's not in the database anyways. NEXT!
continue;
}
/**
* Notifies that a non-option setting has been deleted.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'shipping_class',
'action' => 'delete',
)
);
wp_delete_term( $term_id, 'product_shipping_class' );
continue;
}
@ -3426,9 +3486,27 @@ class WC_AJAX {
if ( empty( $update_args['name'] ) ) {
continue;
}
/**
* Notifies that a non-option setting has been added.
*
* @since 7.8.0
*/
do_action(
'woocommerce_update_non_option_setting',
array(
'id' => 'shipping_class',
'action' => 'add',
)
);
$inserted_term = wp_insert_term( $update_args['name'], 'product_shipping_class', $update_args );
$term_id = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id'];
} else {
/**
* Notifies that a non-option setting has been updated.
*
* @since 7.8.0
*/
do_action( 'woocommerce_update_non_option_setting', array( 'id' => 'shipping_class' ) );
wp_update_term( $term_id, 'product_shipping_class', $update_args );
}

View File

@ -12,8 +12,8 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };
use Automattic\WooCommerce\Internal\Utilities\BlocksUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
@ -153,7 +153,6 @@ class WC_Tracker {
$data['inactive_plugins'] = $all_plugins['inactive_plugins'];
// Jetpack & WooCommerce Connect.
$data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none';
$data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no';
$data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no';
@ -177,6 +176,9 @@ class WC_Tracker {
// Shipping method info.
$data['shipping_methods'] = self::get_active_shipping_methods();
// Features.
$data['enabled_features'] = self::get_enabled_features();
// Get all WooCommerce options info.
$data['settings'] = self::get_all_woocommerce_options_values();
@ -329,6 +331,10 @@ class WC_Tracker {
if ( isset( $v['PluginURI'] ) ) {
$formatted['plugin_uri'] = wp_strip_all_tags( $v['PluginURI'] );
}
$formatted['feature_compatibility'] = array();
if ( wc_get_container()->get( PluginUtil::class )->is_woocommerce_aware_plugin( $k ) ) {
$formatted['feature_compatibility'] = array_filter( FeaturesUtil::get_compatible_features_for_plugin( $k ) );
}
if ( in_array( $k, $active_plugins_keys, true ) ) {
// Remove active plugins from list so we can show active and inactive separately.
unset( $plugins[ $k ] );
@ -904,6 +910,23 @@ class WC_Tracker {
return $active_methods;
}
/**
* Get an array of slugs for WC features that are enabled on the site.
*
* @return string[]
*/
private static function get_enabled_features() {
$all_features = FeaturesUtil::get_features( true, true );
$enabled_features = array_filter(
$all_features,
function( $feature ) {
return $feature['is_enabled'];
}
);
return array_keys( $enabled_features );
}
/**
* Get all options starting with woocommerce_ prefix.
*

View File

@ -204,6 +204,22 @@ final class WooCommerce {
do_action( 'woocommerce_loaded' );
}
/**
* Initiali Jetpack Connection Config.
*
* @return void
*/
public function init_jetpack_connection_config() {
$config = new Automattic\Jetpack\Config();
$config->ensure(
'connection',
array(
'slug' => 'woocommerce',
'name' => __( 'WooCommerce', 'woocommerce' ),
)
);
}
/**
* Hook into actions and filters.
*
@ -214,6 +230,7 @@ final class WooCommerce {
register_shutdown_function( array( $this, 'log_errors' ) );
add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 );
add_action( 'plugins_loaded', array( $this, 'init_jetpack_connection_config' ), 1 );
add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) );
add_action( 'after_setup_theme', array( $this, 'setup_environment' ) );
add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 );

View File

@ -43,6 +43,21 @@ class WC_Settings_Tracking {
*/
protected $modified_options = array();
/**
* List of options that have been deleted.
*
* @var array
*/
protected $deleted_options = array();
/**
* List of options that have been added.
*
* @var array
*/
protected $added_options = array();
/**
* Toggled options.
*
@ -74,7 +89,11 @@ class WC_Settings_Tracking {
if ( ! in_array( $option['id'], $this->allowed_options, true ) ) {
$this->allowed_options[] = $option['id'];
}
if ( ! in_array( $option['id'], $this->updated_options, true ) ) {
if ( 'add' === $option['action'] ) {
$this->added_options[] = $option['id'];
} elseif ( 'delete' === $option['action'] ) {
$this->deleted_options[] = $option['id'];
} elseif ( ! in_array( $option['id'], $this->updated_options, true ) ) {
$this->updated_options[] = $option['id'];
}
}
@ -143,13 +162,23 @@ class WC_Settings_Tracking {
public function send_settings_change_event() {
global $current_tab, $current_section;
if ( empty( $this->updated_options ) ) {
if ( empty( $this->updated_options ) && empty( $this->deleted_options ) && empty( $this->added_options ) ) {
return;
}
$properties = array(
'settings' => implode( ',', $this->updated_options ),
);
$properties = array();
if ( ! empty( $this->updated_options ) ) {
$properties['settings'] = implode( ',', $this->updated_options );
}
if ( ! empty( $this->deleted_options ) ) {
$properties['deleted'] = implode( ',', $this->deleted_options );
}
if ( ! empty( $this->added_options ) ) {
$properties['added'] = implode( ',', $this->added_options );
}
foreach ( $this->toggled_options as $state => $options ) {
if ( ! empty( $options ) ) {

View File

@ -10,6 +10,7 @@ namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
use ActionScheduler;
use Automattic\Jetpack\Connection\Manager;
use Automattic\WooCommerce\Admin\PluginsHelper;
use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsynPluginsInstallLogger;
use WC_REST_Data_Controller;
@ -95,6 +96,34 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
'schema' => array( $this, 'get_install_async_schema' ),
)
);
// This is an experimental endpoint and is subject to change in the future.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/jetpack-authorization-url',
array(
array(
'methods' => 'GET',
'callback' => array( $this, 'get_jetpack_authorization_url' ),
'permission_callback' => array( $this, 'can_install_plugins' ),
'args' => array(
'redirect_url' => array(
'description' => 'The URL to redirect to after authorization',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'required' => true,
),
'from' => array(
'description' => 'from value for the jetpack authorization page',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'required' => false,
'default' => 'woocommerce-onboarding',
),
),
),
)
);
}
/**
@ -183,6 +212,43 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
return $response;
}
/**
* Return Jetpack authorization URL.
*
* @param WP_REST_Request $request WP_REST_Request object.
*
* @return array
* @throws \Exception If there is an error registering the site.
*/
public function get_jetpack_authorization_url( WP_REST_Request $request ) {
$manager = new Manager( 'woocommerce' );
$errors = new WP_Error();
// Register the site to wp.com.
if ( ! $manager->is_connected() ) {
$result = $manager->try_registration();
if ( is_wp_error( $result ) ) {
$errors->add( $result->get_error_code(), $result->get_error_message() );
}
}
$redirect_url = $request->get_param( 'redirect_url' );
$calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, [ 'development', 'wpcalypso', 'horizon', 'stage' ], true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production';
return [
'success' => ! $errors->has_errors(),
'errors' => $errors->get_error_messages(),
'url' => add_query_arg(
[
'from' => $request->get_param( 'from' ),
'calypso_env' => $calypso_env,
],
$manager->get_authorization_url( null, $redirect_url )
),
];
}
/**
* Check whether the current user has permission to install plugins
*

View File

@ -108,21 +108,27 @@ class TaskLists {
* Initialize default lists.
*/
public static function init_default_lists() {
$tasks = array(
'StoreDetails',
'Purchase',
'Products',
'WooCommercePayments',
'Payments',
'Tax',
'Shipping',
'Marketing',
'Appearance',
);
if ( Features::is_enabled( 'core-profiler' ) ) {
array_shift( $tasks );
}
self::add_list(
array(
'id' => 'setup',
'title' => __( 'Get ready to start selling', 'woocommerce' ),
'tasks' => array(
'StoreDetails',
'Purchase',
'Products',
'WooCommercePayments',
'Payments',
'Tax',
'Shipping',
'Marketing',
'Appearance',
),
'tasks' => $tasks,
'display_progress_header' => true,
'event_prefix' => 'tasklist_',
'options' => array(

View File

@ -304,6 +304,11 @@ class CLIRunner {
* ---
* default: Output of function `wc_get_order_types( 'cot-migration' )`
*
* [--re-migrate]
* : Attempt to re-migrate orders that failed verification. You should only use this option when you have never run the site with HPOS as authoritative source of order data yet, or you have manually checked the reported errors, otherwise, you risk stale data overwriting the more recent data.
* This option can only be enabled when --verbose flag is also set.
* default: false
*
* ## EXAMPLES
*
* # Verify migrated order data, 500 orders at a time.
@ -327,6 +332,7 @@ class CLIRunner {
'end-at' => - 1,
'verbose' => false,
'order-types' => '',
're-migrate' => false,
)
);
@ -340,6 +346,7 @@ class CLIRunner {
$batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
$verbose = (bool) $assoc_args['verbose'];
$order_types = wc_get_order_types( 'cot-migration' );
$remigrate = (bool) $assoc_args['re-migrate'];
if ( ! empty( $assoc_args['order-types'] ) ) {
$passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) );
$order_types = array_intersect( $order_types, $passed_order_types );
@ -415,6 +422,36 @@ class CLIRunner {
$errors
)
);
if ( $remigrate ) {
WP_CLI::warning(
sprintf(
__( 'Attempting to remigrate...', 'woocommerce' )
)
);
$failed_ids = array_keys( $failed_ids_in_current_batch );
$this->synchronizer->process_batch( $failed_ids );
$errors_in_remigrate_batch = $this->post_to_cot_migrator->verify_migrated_orders( $failed_ids );
$errors_in_remigrate_batch = $this->verify_meta_data( $failed_ids, $errors_in_remigrate_batch );
if ( count( $errors_in_remigrate_batch ) > 0 ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r -- This is a CLI command and debugging code is intended.
$formatted_errors = print_r( $errors_in_remigrate_batch, true );
WP_CLI::warning(
sprintf(
/* Translators: %1$d is number of errors and %2$s is the formatted array of order IDs. */
_n(
'%1$d error found: %2$s when re-migrating order. Please review the error above.',
'%1$d errors found: %2$s when re-migrating orders. Please review the errors above.',
count( $errors_in_remigrate_batch ),
'woocommerce'
),
count( $errors_in_remigrate_batch ),
$formatted_errors
)
);
} else {
WP_CLI::warning( 'Re-migration successful.', 'woocommerce' );
}
}
}
$progress->tick();

View File

@ -243,13 +243,7 @@ abstract class MetaToCustomTableMigrator extends TableMigrator {
$to_insert = array_diff_key( $data['data'], $existing_records );
$this->process_insert_batch( $to_insert );
$existing_records = array_filter(
$existing_records,
function( $record_data ) {
return '1' === $record_data->modified;
}
);
$to_update = array_intersect_key( $data['data'], $existing_records );
$to_update = array_intersect_key( $data['data'], $existing_records );
$this->process_update_batch( $to_update, $existing_records );
}
@ -357,38 +351,13 @@ abstract class MetaToCustomTableMigrator extends TableMigrator {
$entity_id_placeholder = implode( ',', array_fill( 0, count( $entity_ids ), '%d' ) );
// Additional SQL to check if the row needs update according to the column mapping.
// The IFNULL and CHAR(0) "hack" is needed because NULLs can't be directly compared in SQL.
$modified_selector = array();
$core_column_mapping = array_filter(
$this->core_column_mapping,
function( $mapping ) {
return ! isset( $mapping['select_clause'] );
}
);
foreach ( $core_column_mapping as $column_name => $mapping ) {
if ( $column_name === $source_primary_key_column ) {
continue;
}
$modified_selector[] =
"IFNULL(source.$column_name,CHAR(0)) != IFNULL(destination.{$mapping['destination']},CHAR(0))"
. ( 'string' === $mapping['type'] ? ' COLLATE ' . $wpdb->collate : '' );
}
if ( empty( $modified_selector ) ) {
$modified_selector = ', 1 AS modified';
} else {
$modified_selector = trim( implode( ' OR ', $modified_selector ) );
$modified_selector = ", if( $modified_selector, 1, 0 ) AS modified";
}
$additional_where = $this->get_additional_where_clause_for_get_data_to_insert_or_update( $entity_ids );
$already_migrated_entity_ids = $this->db_get_results(
$wpdb->prepare(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
"
SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id $modified_selector
SELECT source.`$source_primary_key_column` as source_id, destination.`$destination_primary_key_column` as destination_id
FROM `$destination_table` destination
JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column`
WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) $additional_where

View File

@ -6,6 +6,7 @@
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
/**
* Class Edit.
@ -26,6 +27,13 @@ class Edit {
*/
private $custom_meta_box;
/**
* Instance of the TaxonomiesMetaBox class. Used to render meta box for taxonomies.
*
* @var TaxonomiesMetaBox
*/
private $taxonomies_meta_box;
/**
* Instance of WC_Order to be used in metaboxes.
*
@ -110,10 +118,16 @@ class Edit {
if ( ! isset( $this->custom_meta_box ) ) {
$this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class );
}
if ( ! isset( $this->taxonomies_meta_box ) ) {
$this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class );
}
$this->add_save_meta_boxes();
$this->handle_order_update();
$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
$this->add_order_specific_meta_box();
$this->add_order_taxonomies_meta_box();
/**
* From wp-admin/includes/meta-boxes.php.
@ -159,6 +173,15 @@ class Edit {
);
}
/**
* Render custom meta box.
*
* @return void
*/
private function add_order_taxonomies_meta_box() {
$this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() );
}
/**
* Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
*
@ -176,6 +199,10 @@ class Edit {
check_admin_referer( $this->get_order_edit_nonce_action() );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object.
$taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null;
$this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input );
/**
* Save meta for shop order.
*

View File

@ -0,0 +1,147 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
/**
* TaxonomiesMetaBox class, renders taxonomy sidebar widget on order edit screen.
*/
class TaxonomiesMetaBox {
/**
* Order Table data store class.
*
* @var OrdersTableDataStore
*/
private $orders_table_data_store;
/**
* Dependency injection init method.
*
* @param OrdersTableDataStore $orders_table_data_store Order Table data store class.
*
* @return void
*/
public function init( OrdersTableDataStore $orders_table_data_store ) {
$this->orders_table_data_store = $orders_table_data_store;
}
/**
* Registers meta boxes to be rendered in order edit screen for taxonomies.
*
* Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it.
*
* @param string $screen_id Screen ID.
* @param string $order_type Order type to register meta boxes for.
*
* @return void
*/
public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) {
include_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
$taxonomies = get_object_taxonomies( $order_type );
// All taxonomies.
foreach ( $taxonomies as $tax_name ) {
$taxonomy = get_taxonomy( $tax_name );
if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) {
continue;
}
if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) {
$taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' );
}
if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) {
$taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' );
}
$label = $taxonomy->labels->name;
if ( ! is_taxonomy_hierarchical( $tax_name ) ) {
$tax_meta_box_id = 'tagsdiv-' . $tax_name;
} else {
$tax_meta_box_id = $tax_name . 'div';
}
add_meta_box(
$tax_meta_box_id,
$label,
$taxonomy->meta_box_cb,
$screen_id,
'side',
'core',
array(
'taxonomy' => $tax_name,
'__back_compat_meta_box' => true,
)
);
}
}
/**
* Save handler for taxonomy data.
*
* @param \WC_Abstract_Order $order Order object.
* @param array|null $taxonomy_input Taxonomy input passed from input.
*/
public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) {
if ( ! isset( $taxonomy_input ) ) {
return;
}
$sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input );
$sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input );
$this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input );
}
/**
* Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy.
*
* @param array|null $taxonomy_data Nonce verified taxonomy input.
*
* @return array Sanitized taxonomy input.
*/
private function sanitize_tax_input( $taxonomy_data ) : array {
$sanitized_tax_input = array();
if ( ! is_array( $taxonomy_data ) ) {
return $sanitized_tax_input;
}
// Convert taxonomy input to term IDs, to avoid ambiguity.
foreach ( $taxonomy_data as $taxonomy => $terms ) {
$tax_object = get_taxonomy( $taxonomy );
if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) {
$sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) );
}
}
return $sanitized_tax_input;
}
/**
* Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box.
*
* @param \WC_Abstract_Order $order Order object.
* @param array $box Meta box args.
*
* @return void
*/
public function order_categories_meta_box( $order, $box ) {
$post = get_post( $order->get_id() );
post_categories_meta_box( $post, $box );
}
/**
* Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box.
*
* @param \WC_Abstract_Order $order Order object.
* @param array $box Meta box args.
*
* @return void
*/
public function order_tags_meta_box( $order, $box ) {
$post = get_post( $order->get_id() );
post_tags_meta_box( $post, $box );
}
}

View File

@ -1641,6 +1641,84 @@ FROM $order_meta_table
$changes = $order->get_changes();
$this->update_address_index_meta( $order, $changes );
$default_taxonomies = $this->init_default_taxonomies( $order, array() );
$this->set_custom_taxonomies( $order, $default_taxonomies );
}
/**
* Set default taxonomies for the order.
*
* Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set default taxonomies is not filterable, we have to re-implement it.
*
* @param \WC_Abstract_Order $order Order object.
* @param array $sanitized_tax_input Sanitized taxonomy input.
*
* @return array Sanitized tax input with default taxonomies.
*/
public function init_default_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) {
if ( 'auto-draft' === $order->get_status() ) {
return $sanitized_tax_input;
}
foreach ( get_object_taxonomies( $order->get_type(), 'object' ) as $taxonomy => $tax_object ) {
if ( empty( $tax_object->default_term ) ) {
return $sanitized_tax_input;
}
// Filter out empty terms.
if ( isset( $sanitized_tax_input[ $taxonomy ] ) && is_array( $sanitized_tax_input[ $taxonomy ] ) ) {
$sanitized_tax_input[ $taxonomy ] = array_filter( $sanitized_tax_input[ $taxonomy ] );
}
// Passed custom taxonomy list overwrites the existing list if not empty.
$terms = wp_get_object_terms( $order->get_id(), $taxonomy, array( 'fields' => 'ids' ) );
if ( ! empty( $terms ) && empty( $sanitized_tax_input[ $taxonomy ] ) ) {
$sanitized_tax_input[ $taxonomy ] = $terms;
}
if ( empty( $sanitized_tax_input[ $taxonomy ] ) ) {
$default_term_id = get_option( 'default_term_' . $taxonomy );
if ( ! empty( $default_term_id ) ) {
$sanitized_tax_input[ $taxonomy ] = array( (int) $default_term_id );
}
}
}
return $sanitized_tax_input;
}
/**
* Set custom taxonomies for the order.
*
* Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set custom taxonomies is not filterable, we have to re-implement it.
*
* @param \WC_Abstract_Order $order Order object.
* @param array $sanitized_tax_input Sanitized taxonomy input.
*
* @return void
*/
public function set_custom_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) {
if ( empty( $sanitized_tax_input ) ) {
return;
}
foreach ( $sanitized_tax_input as $taxonomy => $tags ) {
$taxonomy_obj = get_taxonomy( $taxonomy );
if ( ! $taxonomy_obj ) {
/* translators: %s: Taxonomy name. */
_doing_it_wrong( __FUNCTION__, esc_html( sprintf( __( 'Invalid taxonomy: %s.', 'woocommerce' ), $taxonomy ) ), '7.9.0' );
continue;
}
// array = hierarchical, string = non-hierarchical.
if ( is_array( $tags ) ) {
$tags = array_filter( $tags );
}
if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) {
wp_set_post_terms( $order->get_id(), $tags, $taxonomy );
}
}
}
/**
@ -1738,8 +1816,8 @@ FROM $order_meta_table
$changes['type'] = $order->get_type();
// Make sure 'status' is correct.
if ( array_key_exists( 'status', $column_mapping ) ) {
// Make sure 'status' is correctly prefixed.
if ( array_key_exists( 'status', $column_mapping ) && array_key_exists( 'status', $changes ) ) {
$changes['status'] = $this->get_post_status( $order );
}

View File

@ -9,7 +9,9 @@ use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController;
use Automattic\WooCommerce\Internal\Admin\Orders\Edit;
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
/**
@ -28,6 +30,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
Edit::class,
ListTable::class,
EditLock::class,
TaxonomiesMetaBox::class,
);
/**
@ -41,5 +44,6 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
$this->share( Edit::class )->addArgument( PageController::class );
$this->share( ListTable::class )->addArgument( PageController::class );
$this->share( EditLock::class );
$this->share( TaxonomiesMetaBox::class )->addArgument( OrdersTableDataStore::class );
}
}

View File

@ -264,6 +264,7 @@ class WC_Unit_Tests_Bootstrap {
// Traits.
require_once $this->tests_dir . '/framework/traits/trait-wc-rest-api-complex-meta.php';
require_once dirname( $this->tests_dir ) . '/php/helpers/HPOSToggleTrait.php';
}
/**

View File

@ -2,12 +2,8 @@
namespace Automattic\WooCommerce\RestApi\UnitTests;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use WC_Data_Store;
/**
* Trait HPOSToggleTrait.

View File

@ -190,7 +190,7 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
*/
public function test_apply_coupon_across_status() {
$coupon_code = 'coupon_test_count_across_status';
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
$this->assertEquals( 0, $coupon->get_usage_count() );
$order = WC_Helper_Order::create_order();
@ -253,8 +253,8 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
*/
public function test_apply_coupon_stores_meta_data() {
$coupon_code = 'coupon_test_meta_data';
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
$order = WC_Helper_Order::create_order();
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
$order = WC_Helper_Order::create_order();
$order->set_status( 'processing' );
$order->save();
$order->apply_coupon( $coupon_code );
@ -324,4 +324,29 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
$order = wc_get_order( $order->get_id() );
$this->assertInstanceOf( Automattic\WooCommerce\Admin\Overrides\Order::class, $order );
}
/**
* @testDox When a taxonomy with a default term is set on the order, it's inserted when a new order is created.
*/
public function test_default_term_for_custom_taxonomy() {
$custom_taxonomy = register_taxonomy(
'custom_taxonomy',
'shop_order',
array(
'default_term' => 'new_term',
),
);
// Set user who has access to create term.
$current_user_id = get_current_user_id();
$user = new WP_User( wp_create_user( 'test', '' ) );
$user->set_role( 'administrator' );
wp_set_current_user( $user->ID );
$order = wc_create_order();
wp_set_current_user( $current_user_id );
$order_terms = wp_list_pluck( wp_get_object_terms( $order->get_id(), $custom_taxonomy->name ), 'name' );
$this->assertContains( 'new_term', $order_terms );
}
}

View File

@ -118,4 +118,13 @@ class WC_Tracker_Test extends \WC_Unit_Test_Case {
$this->assertEquals( ( $order_count / count( $created_via_entries ) ), $order_data['created_via'][ $created_via_entry ] );
}
}
/**
* @testDox Test enabled features tracking data.
*/
public function test_get_tracking_data_enabled_features() {
$tracking_data = WC_Tracker::get_tracking_data();
$this->assertIsArray( $tracking_data['enabled_features'] );
}
}

View File

@ -9,8 +9,6 @@ use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
use Automattic\WooCommerce\Utilities\OrderUtil;
require_once __DIR__ . '/../../../../helpers/HPOSToggleTrait.php';
/**
* Class OrdersTableDataStoreTests.
*
@ -2132,4 +2130,20 @@ class OrdersTableDataStoreTests extends WC_Unit_Test_Case {
$this->assertEquals( 1, $result );
}
/**
* @testDox When saving an order, status is automatically prefixed even if it was not earlier.
*/
public function test_get_db_row_from_order_only_prefixed_status_is_written_to_db() {
$order = wc_create_order();
$order->set_status( 'completed' );
$db_row_callback = function ( $order, $only_changes ) {
return $this->get_db_row_from_order( $order, $this->order_column_mapping, $only_changes );
};
$db_row = $db_row_callback->call( $this->sut, $order, false );
$this->assertEquals( 'wc-completed', $db_row['data']['status'] );
}
}

View File

@ -60,3 +60,8 @@ function wc_get_container() {
// Global for backwards compatibility.
$GLOBALS['woocommerce'] = WC();
// Jetpack's Rest_Authentication needs to be initialized even before plugins_loaded.
if ( class_exists( \Automattic\Jetpack\Connection\Rest_Authentication::class ) ) {
\Automattic\Jetpack\Connection\Rest_Authentication::init();
}

View File

@ -9,9 +9,9 @@
* Register the JS and CSS.
*/
function add_extension_register_script() {
if (
! method_exists( 'Automattic\WooCommerce\Admin\Loader', 'is_admin_or_embed_page' ) ||
! \Automattic\WooCommerce\Admin\Loader::is_admin_or_embed_page()
if (
! method_exists( 'Automattic\WooCommerce\Admin\PageController', 'is_admin_or_embed_page' ) ||
! \Automattic\WooCommerce\Admin\PageController::is_admin_or_embed_page()
) {
return;
}