diff --git a/.github/actions/setup-woocommerce-monorepo/action.yml b/.github/actions/setup-woocommerce-monorepo/action.yml index 323977de415..2bff7208edd 100644 --- a/.github/actions/setup-woocommerce-monorepo/action.yml +++ b/.github/actions/setup-woocommerce-monorepo/action.yml @@ -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 diff --git a/changelog.txt b/changelog.txt index 36de64fa26d..5e06af3fa54 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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** diff --git a/packages/js/customer-effort-score/changelog/fix-38502_ces_modal_styling_fixes b/packages/js/customer-effort-score/changelog/fix-38502_ces_modal_styling_fixes new file mode 100644 index 00000000000..2aad72e0199 --- /dev/null +++ b/packages/js/customer-effort-score/changelog/fix-38502_ces_modal_styling_fixes @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Add extraFields and showDescription props diff --git a/packages/js/customer-effort-score/src/components/customer-effort-score-modal-container/index.tsx b/packages/js/customer-effort-score/src/components/customer-effort-score-modal-container/index.tsx index a5fb684edfc..eda4a393793 100644 --- a/packages/js/customer-effort-score/src/components/customer-effort-score-modal-container/index.tsx +++ b/packages/js/customer-effort-score/src/components/customer-effort-score-modal-container/index.tsx @@ -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 ( { @@ -87,6 +90,10 @@ export const CustomerEffortScoreModalContainer: React.FC = () => { hideCesModal(); } } shouldShowComments={ visibleCESModalData.props?.shouldShowComments } + getExtraFieldsToBeShown={ + visibleCESModalData.getExtraFieldsToBeShown + } + validateExtraFields={ visibleCESModalData.validateExtraFields } /> ); }; diff --git a/packages/js/customer-effort-score/src/components/customer-effort-score/index.tsx b/packages/js/customer-effort-score/src/components/customer-effort-score/index.tsx index 74fec992fc9..ce41f91feec 100644 --- a/packages/js/customer-effort-score/src/components/customer-effort-score/index.tsx +++ b/packages/js/customer-effort-score/src/components/customer-effort-score/index.tsx @@ -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 > = ( { ); }; diff --git a/packages/js/customer-effort-score/src/components/customer-feedback-modal/index.tsx b/packages/js/customer-effort-score/src/components/customer-feedback-modal/index.tsx index ad894742cf6..49d55caca80 100644 --- a/packages/js/customer-effort-score/src/components/customer-feedback-modal/index.tsx +++ b/packages/js/customer-effort-score/src/components/customer-feedback-modal/index.tsx @@ -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 } > - - { 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' - ) } - + { showDescription && ( + + { 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' + ) } + + ) } { __( - 'Please provide feedback by selecting an option above.', + 'Please tell us to what extent you agree or disagree with the statements above.', 'woocommerce' ) } ) } + { typeof getExtraFieldsToBeShown === 'function' && + getExtraFieldsToBeShown( + extraFieldsValues, + setExtraFieldsValues, + errors + ) } +
- + shareButton: ( + + ) } ); diff --git a/packages/js/product-editor/src/components/feedback-bar/style.scss b/packages/js/product-editor/src/components/feedback-bar/style.scss index b588e6f81bf..24878c2b9ff 100644 --- a/packages/js/product-editor/src/components/feedback-bar/style.scss +++ b/packages/js/product-editor/src/components/feedback-bar/style.scss @@ -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; + } + } + } +} diff --git a/packages/js/product-editor/src/components/footer/footer.tsx b/packages/js/product-editor/src/components/footer/footer.tsx index 664c0eb2d1a..45eea7df088 100644 --- a/packages/js/product-editor/src/components/footer/footer.tsx +++ b/packages/js/product-editor/src/components/footer/footer.tsx @@ -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 ( -
- - - - -
+ + <> + + + + ); } diff --git a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx index e194ffb124a..195d625b1f8 100644 --- a/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx +++ b/packages/js/product-editor/src/components/iframe-editor/header-toolbar/header-toolbar.tsx @@ -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 && ( + + ) } diff --git a/packages/js/product-editor/src/hooks/use-currency-input-props.ts b/packages/js/product-editor/src/hooks/use-currency-input-props.ts index f92e141bae7..b4aabcd0d09 100644 --- a/packages/js/product-editor/src/hooks/use-currency-input-props.ts +++ b/packages/js/product-editor/src/hooks/use-currency-input-props.ts @@ -81,7 +81,7 @@ export const useCurrencyInputProps = ( { } }, onChange( newValue: string ) { - const sanitizeValue = sanitizePrice( newValue || '0' ); + const sanitizeValue = sanitizePrice( newValue ); if ( onChange ) { onChange( sanitizeValue ); } diff --git a/packages/js/product-editor/src/hooks/use-product-helper.ts b/packages/js/product-editor/src/hooks/use-product-helper.ts index b55178bd511..fc8da5d5a02 100644 --- a/packages/js/product-editor/src/hooks/use-product-helper.ts +++ b/packages/js/product-editor/src/hooks/use-product-helper.ts @@ -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. diff --git a/packages/js/product-editor/src/utils/index.ts b/packages/js/product-editor/src/utils/index.ts index b4ccb700166..a546a1a8653 100644 --- a/packages/js/product-editor/src/utils/index.ts +++ b/packages/js/product-editor/src/utils/index.ts @@ -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, }; diff --git a/packages/js/product-editor/src/utils/validate-email.ts b/packages/js/product-editor/src/utils/validate-email.ts new file mode 100644 index 00000000000..28288d0ea57 --- /dev/null +++ b/packages/js/product-editor/src/utils/validate-email.ts @@ -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() ); +}; diff --git a/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx b/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx index 944fc584ed4..9691ee441cd 100644 --- a/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/actions/tracks.tsx @@ -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, }; diff --git a/plugins/woocommerce-admin/client/core-profiler/components/plugin-card/plugin-card.scss b/plugins/woocommerce-admin/client/core-profiler/components/plugin-card/plugin-card.scss index 33297fb17c4..eb3e18cd89d 100644 --- a/plugins/woocommerce-admin/client/core-profiler/components/plugin-card/plugin-card.scss +++ b/plugins/woocommerce-admin/client/core-profiler/components/plugin-card/plugin-card.scss @@ -20,6 +20,10 @@ margin-top: 6px; } + .components-checkbox-control__input-container { + margin-top: 6px; + } + input { margin: 3px 26px 0 0; width: 20px; diff --git a/plugins/woocommerce-admin/client/core-profiler/index.tsx b/plugins/woocommerce-admin/client/core-profiler/index.tsx index 9e047cbecd7..4200c77a7b0 100644 --- a/plugins/woocommerce-admin/client/core-profiler/index.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/index.tsx @@ -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 ?? ( () => ); // 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 = ( {
- { + { CurrentComponent ? ( - } + ) : ( + + ) }
); diff --git a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx index af1aecb297c..8218f26b5a9 100644 --- a/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx +++ b/plugins/woocommerce-admin/client/core-profiler/pages/BusinessInfo.tsx @@ -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 ] = diff --git a/plugins/woocommerce-admin/client/layout/footer/footer.scss b/plugins/woocommerce-admin/client/layout/footer/footer.scss index 4970b110467..fa438b5e85c 100644 --- a/plugins/woocommerce-admin/client/layout/footer/footer.scss +++ b/plugins/woocommerce-admin/client/layout/footer/footer.scss @@ -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); diff --git a/plugins/woocommerce-admin/client/layout/index.js b/plugins/woocommerce-admin/client/layout/index.js index 44399423a5b..c3d54550376 100644 --- a/plugins/woocommerce-admin/client/layout/index.js +++ b/plugins/woocommerce-admin/client/layout/index.js @@ -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'; diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/index.js b/plugins/woocommerce-admin/client/layout/transient-notices/index.js index d4920632b5b..33be50769fd 100644 --- a/plugins/woocommerce-admin/client/layout/transient-notices/index.js +++ b/plugins/woocommerce-admin/client/layout/transient-notices/index.js @@ -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 ( - + + + ); } @@ -108,5 +111,3 @@ TransientNotices.propTypes = { */ notices: PropTypes.array, }; - -export default TransientNotices; diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/style.scss b/plugins/woocommerce-admin/client/layout/transient-notices/style.scss index ce44b0bd7a9..94a3ed2686d 100644 --- a/plugins/woocommerce-admin/client/layout/transient-notices/style.scss +++ b/plugins/woocommerce-admin/client/layout/transient-notices/style.scss @@ -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 { diff --git a/plugins/woocommerce-admin/client/layout/transient-notices/test/index.js b/plugins/woocommerce-admin/client/layout/transient-notices/test/index.js index 99f52f5087c..863134b5a66 100644 --- a/plugins/woocommerce-admin/client/layout/transient-notices/test/index.js +++ b/plugins/woocommerce-admin/client/layout/transient-notices/test/index.js @@ -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
{ children }
; + } ), + }; +} ); + jest.mock( '../snackbar/list', () => jest.fn( ( { notices } ) => { return notices.map( ( notice ) => ( diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/about-the-editor-menu-item.tsx b/plugins/woocommerce-admin/client/products/fills/more-menu-items/about-the-editor-menu-item.tsx new file mode 100644 index 00000000000..864f5610ef1 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/about-the-editor-menu-item.tsx @@ -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 ( + <> + { + recordEvent( + 'block_product_editor_about_the_editor_menu_item_clicked' + ); + setIsGuideOpen( true ); + } } + icon={ } + iconPosition="right" + > + { __( 'About the editor…', 'woocommerce' ) } + + { isGuideOpen && ( + { + setIsGuideOpen( false ); + onClose(); + } } + /> + ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx b/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx index 66d75947731..fd050231a7d 100644 --- a/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/classic-editor-menu-item.tsx @@ -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 ( { - if ( allowTracking ) { - showProductMVPFeedbackModal(); - } else { - window.location.href = classicEditorUrl; - } - onClose(); - } } - icon={ } - 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' ) } ); }; diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx b/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx index 4bf617987f2..b6a12497ada 100644 --- a/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/feedback-menu-item.tsx @@ -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 + ) => ( + + ', + 'woocommerce' + ), + { + optional: ( + + { __( + '(OPTIONAL)', + 'woocommerce' + ) } + + ), + } + ) } + > + + setValues( { + ...values, + additional_thoughts: value, + } ) + } + help={ + errors?.additional_thoughts || '' + } + /> + + ', + 'woocommerce' + ), + { + optional: ( + + { __( + '(OPTIONAL)', + 'woocommerce' + ) } + + ), + } + ) } + > + + setValues( { + ...values, + email: value, + } ) + } + help={ errors?.email || '' } + /> + + { __( + 'In case you want to participate in further discussion and future user research.', + 'woocommerce' + ) } + + + + ), + 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', diff --git a/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts b/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts index c180f511b00..e90c38d1877 100644 --- a/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts +++ b/plugins/woocommerce-admin/client/products/fills/more-menu-items/index.ts @@ -1,2 +1,3 @@ export * from './feedback-menu-item'; export * from './classic-editor-menu-item'; +export * from './about-the-editor-menu-item'; diff --git a/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx b/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx index 4f56f880ba1..ed33f72d9e0 100644 --- a/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/product-block-editor-fills.tsx @@ -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 } ) => { <> + ); }; diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-guide.tsx b/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-guide.tsx index 05b0f5f3d7e..1f32347a68f 100644 --- a/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-guide.tsx +++ b/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-guide.tsx @@ -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; + }; +} ) => ( + <> +

+ { page.heading } +

+

{ page.text }

+ +); + +const PageImage = ( { + page, +}: { + page: { + index: number; + }; +} ) => ( +
+); + +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: , + image: , + } ) ); + return ( { finishButtonText={ __( 'Tell me more', 'woocommerce' ) } finishButtonLink="https://woocommerce.com/product-form-beta" onFinish={ onCloseGuide } - pages={ [ - { - content: ( - <> -

- { __( - 'Refreshed, streamlined interface', - 'woocommerce' - ) } -

-

- { __( - 'Experience a simpler, more focused interface with a modern design that enhances usability.', - 'woocommerce' - ) } -

- - ), - image: ( -
- ), - }, - { - content: ( - <> -

- { __( - 'Content-rich product descriptions', - 'woocommerce' - ) } -

-

- { __( - 'Create compelling product pages with blocks, media, images, videos, and any content you desire to engage customers.', - 'woocommerce' - ) } -

- - ), - image: ( -
- ), - }, - { - content: ( - <> -

- { __( - 'Improved speed & performance', - 'woocommerce' - ) } -

-

- { __( - 'Enjoy a seamless experience without page reloads. Our modern technology ensures reliability and lightning-fast performance.', - 'woocommerce' - ) } -

- - ), - image: ( -
- ), - }, - { - content: ( - <> -

- { __( - 'More features are on the way', - 'woocommerce' - ) } -

-

- { __( - 'While we currently support physical products, exciting updates are coming to accommodate more types, like digital products, variations, and more. Stay tuned!', - 'woocommerce' - ) } -

- - ), - image: ( -
- ), - }, - ] } + pages={ pages } /> ); }; diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-tour.tsx b/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-tour.tsx index 59b369f722b..87a2bb5db29 100644 --- a/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-tour.tsx +++ b/plugins/woocommerce-admin/client/products/tour/block-editor/block-editor-tour.tsx @@ -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 ( { dismissModal(); if ( source === 'finish' ) { @@ -58,6 +84,8 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => { /> ); } else if ( shouldTourBeShown ) { + const { heading, description } = getTourText(); + return ( { ), }, descriptions: { - desktop: __( - "We designed a brand new product editing experience to let you focus on what's important.", - 'woocommerce' - ), + desktop: description, }, heading: ( <> - - { __( - 'Meet a streamlined product form', - 'woocommerce' - ) } - { ' ' } - + { heading } + { __( 'Beta', 'woocommerce' ) } diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-1.svg b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-1.svg new file mode 100644 index 00000000000..0954135a334 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-1.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-2.png b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-2.png new file mode 100644 index 00000000000..2f68ecb3263 Binary files /dev/null and b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-2.png differ diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-3.png b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-3.png new file mode 100644 index 00000000000..c4f9b471c98 Binary files /dev/null and b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-3.png differ diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-4.png b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-4.png new file mode 100644 index 00000000000..3afbdf8672b Binary files /dev/null and b/plugins/woocommerce-admin/client/products/tour/block-editor/images/guide-4.png differ diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/images/tour-header.svg b/plugins/woocommerce-admin/client/products/tour/block-editor/images/tour-header.svg new file mode 100644 index 00000000000..ac84b6014e4 --- /dev/null +++ b/plugins/woocommerce-admin/client/products/tour/block-editor/images/tour-header.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/style.scss b/plugins/woocommerce-admin/client/products/tour/block-editor/style.scss index 9ba4967cae3..266820f31ec 100644 --- a/plugins/woocommerce-admin/client/products/tour/block-editor/style.scss +++ b/plugins/woocommerce-admin/client/products/tour/block-editor/style.scss @@ -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; + } + } } + + } diff --git a/plugins/woocommerce-admin/client/products/tour/block-editor/use-published-products-count.tsx b/plugins/woocommerce-admin/client/products/tour/block-editor/use-published-products-count.tsx new file mode 100644 index 00000000000..2a8e939e59a --- /dev/null +++ b/plugins/woocommerce-admin/client/products/tour/block-editor/use-published-products-count.tsx @@ -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, + }; + } ); +}; diff --git a/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts b/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts index aba5bc8e41a..92685995e6a 100644 --- a/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts +++ b/plugins/woocommerce-admin/client/task-lists/fills/products/use-create-product-by-type.ts @@ -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; } diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss b/plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss index 9e47e8b48ff..b5bbbaa07e8 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/style.scss @@ -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; diff --git a/plugins/woocommerce/changelog/add-38575 b/plugins/woocommerce/changelog/add-38575 new file mode 100644 index 00000000000..a81c26ffd1b --- /dev/null +++ b/plugins/woocommerce/changelog/add-38575 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Replace 'use classic editor' with 'Turn off the new product editor' in options menu#38575 diff --git a/plugins/woocommerce/changelog/add-85 b/plugins/woocommerce/changelog/add-85 new file mode 100644 index 00000000000..238f3d3a0b9 --- /dev/null +++ b/plugins/woocommerce/changelog/add-85 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Implement the product blocks experiment within code for new users diff --git a/plugins/woocommerce/changelog/add-about-the-form b/plugins/woocommerce/changelog/add-about-the-form new file mode 100644 index 00000000000..d266121ba3b --- /dev/null +++ b/plugins/woocommerce/changelog/add-about-the-form @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Product Editor Onboarding: Add About the editor... option the more menu in product block editor diff --git a/plugins/woocommerce/changelog/fix-35951 b/plugins/woocommerce/changelog/fix-35951 new file mode 100644 index 00000000000..79e914c393e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35951 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Update status only when it's changed. diff --git a/plugins/woocommerce/changelog/fix-38347_tax_report_count_number_of_orders b/plugins/woocommerce/changelog/fix-38347_tax_report_count_number_of_orders new file mode 100644 index 00000000000..f8c8841d21c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38347_tax_report_count_number_of_orders @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix number of orders under tax report diff --git a/plugins/woocommerce/changelog/fix-38502_ces_modal_styling_fixes b/plugins/woocommerce/changelog/fix-38502_ces_modal_styling_fixes new file mode 100644 index 00000000000..24146a9c446 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38502_ces_modal_styling_fixes @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +CES modal: styling fixes and extraFiels prop added diff --git a/plugins/woocommerce/changelog/fix-38560 b/plugins/woocommerce/changelog/fix-38560 new file mode 100644 index 00000000000..57c57921ada --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38560 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add support for taxonomy meta boxes in HPOS order edit screen. diff --git a/plugins/woocommerce/changelog/fix-38645-checklist-overlap b/plugins/woocommerce/changelog/fix-38645-checklist-overlap new file mode 100644 index 00000000000..6f442b66b9e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38645-checklist-overlap @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fixed a visual bug where text overlapped the image in the task list header. diff --git a/plugins/woocommerce/changelog/fix-38660 b/plugins/woocommerce/changelog/fix-38660 new file mode 100644 index 00000000000..aa634dc5c0c --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38660 @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Add re-migrate support to HPOS CLI. diff --git a/plugins/woocommerce/changelog/fix-38758-fix-jetpack-redirection-when-endpoint-returns-invalid-url b/plugins/woocommerce/changelog/fix-38758-fix-jetpack-redirection-when-endpoint-returns-invalid-url new file mode 100644 index 00000000000..d49c879a9b8 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38758-fix-jetpack-redirection-when-endpoint-returns-invalid-url @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Redirect users to WooCommerce Home when Jetpack auth endpoint returns an invalid URL. diff --git a/plugins/woocommerce/changelog/fix-core-profiler-plugins-race-condition b/plugins/woocommerce/changelog/fix-core-profiler-plugins-race-condition new file mode 100644 index 00000000000..f8c269424bb --- /dev/null +++ b/plugins/woocommerce/changelog/fix-core-profiler-plugins-race-condition @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Fixed race condition in core profiler's plugin list fetching and also minor spinner fixes diff --git a/plugins/woocommerce/changelog/fix-guide-new-user b/plugins/woocommerce/changelog/fix-guide-new-user new file mode 100644 index 00000000000..f110747e0ab --- /dev/null +++ b/plugins/woocommerce/changelog/fix-guide-new-user @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Consider if user is new or not when clicking in "About the editor" + + diff --git a/plugins/woocommerce/changelog/fix-hpos-unit-test-trait b/plugins/woocommerce/changelog/fix-hpos-unit-test-trait new file mode 100644 index 00000000000..863e64c0279 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-hpos-unit-test-trait @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +add HPOSToggleTrait.php to unit test loader diff --git a/plugins/woocommerce/changelog/fix-notices-overlapping-footer-2 b/plugins/woocommerce/changelog/fix-notices-overlapping-footer-2 new file mode 100644 index 00000000000..4a81a431ac5 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-notices-overlapping-footer-2 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix overlapping TransientNotices with footer diff --git a/plugins/woocommerce/changelog/try-wc-tracker-feature-compat b/plugins/woocommerce/changelog/try-wc-tracker-feature-compat new file mode 100644 index 00000000000..6247d668528 --- /dev/null +++ b/plugins/woocommerce/changelog/try-wc-tracker-feature-compat @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Adds info about features and plugin compatibility to the data collected by WC Tracker diff --git a/plugins/woocommerce/changelog/update-38627-update-track-prefix-for-core-profiler b/plugins/woocommerce/changelog/update-38627-update-track-prefix-for-core-profiler new file mode 100644 index 00000000000..651979f4837 --- /dev/null +++ b/plugins/woocommerce/changelog/update-38627-update-track-prefix-for-core-profiler @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Use coreprofiler_ prefix for core profiler track names diff --git a/plugins/woocommerce/changelog/update-38687-update-storedetails-for-coreprofiler b/plugins/woocommerce/changelog/update-38687-update-storedetails-for-coreprofiler new file mode 100644 index 00000000000..8533117e615 --- /dev/null +++ b/plugins/woocommerce/changelog/update-38687-update-storedetails-for-coreprofiler @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove StoreDetails task when core-profiler flag is on diff --git a/plugins/woocommerce/changelog/update-action-scheduler-3.6.1 b/plugins/woocommerce/changelog/update-action-scheduler-3.6.1 new file mode 100644 index 00000000000..7928fbb0f47 --- /dev/null +++ b/plugins/woocommerce/changelog/update-action-scheduler-3.6.1 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update Action Scheduler to 3.6.1. diff --git a/plugins/woocommerce/changelog/update-add-delete-shipping b/plugins/woocommerce/changelog/update-add-delete-shipping new file mode 100644 index 00000000000..af3f692ea97 --- /dev/null +++ b/plugins/woocommerce/changelog/update-add-delete-shipping @@ -0,0 +1,5 @@ +Significance: patch +Type: enhancement +Comment: Add wcadmin_settings_change tracks event when adding/removing entries in shipping + + diff --git a/plugins/woocommerce/changelog/update-implement-connect-to-jetpack b/plugins/woocommerce/changelog/update-implement-connect-to-jetpack new file mode 100644 index 00000000000..5d73df82864 --- /dev/null +++ b/plugins/woocommerce/changelog/update-implement-connect-to-jetpack @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add Jetpack Connection package diff --git a/plugins/woocommerce/changelog/update-product-editor-tour-design b/plugins/woocommerce/changelog/update-product-editor-tour-design new file mode 100644 index 00000000000..14fd1f18ff3 --- /dev/null +++ b/plugins/woocommerce/changelog/update-product-editor-tour-design @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +Update product editor tour/guide copy and style. diff --git a/plugins/woocommerce/changelog/update-skipped-should-be-set b/plugins/woocommerce/changelog/update-skipped-should-be-set new file mode 100644 index 00000000000..560dd7fd4a3 --- /dev/null +++ b/plugins/woocommerce/changelog/update-skipped-should-be-set @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Set woocommerce_onboarding_profile.skipped when guided set is skipped diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index 8bf322f94f4..e7a375af7ed 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -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": { diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 18fbc3d91e1..8569435b321 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -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", diff --git a/plugins/woocommerce/includes/admin/reports/class-wc-report-taxes-by-code.php b/plugins/woocommerce/includes/admin/reports/class-wc-report-taxes-by-code.php index a76375904e1..2e32832d810 100644 --- a/plugins/woocommerce/includes/admin/reports/class-wc-report-taxes-by-code.php +++ b/plugins/woocommerce/includes/admin/reports/class-wc-report-taxes-by-code.php @@ -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, diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php index 7df471ec108..6d66ab1224f 100644 --- a/plugins/woocommerce/includes/class-wc-ajax.php +++ b/plugins/woocommerce/includes/class-wc-ajax.php @@ -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 ); } diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php index a93d25fa469..f51065c2846 100644 --- a/plugins/woocommerce/includes/class-wc-tracker.php +++ b/plugins/woocommerce/includes/class-wc-tracker.php @@ -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. * diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php index f9427ef3f49..830447dc6e5 100644 --- a/plugins/woocommerce/includes/class-woocommerce.php +++ b/plugins/woocommerce/includes/class-woocommerce.php @@ -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 ); diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php index d916ad0488e..7f1dde896cc 100644 --- a/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php +++ b/plugins/woocommerce/includes/tracks/events/class-wc-settings-tracking.php @@ -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 ) ) { diff --git a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php index 6c916695a3b..1c61d635d04 100644 --- a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php +++ b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php @@ -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 * diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php index 8385ee9865a..28e63bce7ad 100644 --- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php +++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/TaskLists.php @@ -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( diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php index 87bc72b7ef7..f2d30be63b0 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/CLIRunner.php @@ -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(); diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index 6f932e842df..ae6c67bdaf3 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -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 diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php index 9ae7e049508..f08f8acec36 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php @@ -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. * diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php new file mode 100644 index 00000000000..f932371aad2 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php @@ -0,0 +1,147 @@ +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 ); + } +} diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 2d25de0d9b3..33f3cb2619a 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -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 ); } diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php index b78acdaf918..e916f54561d 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php @@ -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 ); } } diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php index 273c943e31d..5b0c9363807 100644 --- a/plugins/woocommerce/tests/legacy/bootstrap.php +++ b/plugins/woocommerce/tests/legacy/bootstrap.php @@ -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'; } /** diff --git a/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php b/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php index 4531a3eb28d..0915542c914 100644 --- a/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php +++ b/plugins/woocommerce/tests/php/helpers/HPOSToggleTrait.php @@ -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. diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php index b164ec95a5f..f966aa2e4ca 100644 --- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php +++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php @@ -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 ); + } } diff --git a/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php b/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php index 7f299540c83..41da24722d7 100644 --- a/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php +++ b/plugins/woocommerce/tests/php/includes/class-wc-tracker-test.php @@ -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'] ); + } } diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 23bcd9e8483..ec65db96999 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -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'] ); + } } diff --git a/plugins/woocommerce/woocommerce.php b/plugins/woocommerce/woocommerce.php index e9fe31052e4..f5dd2c476e2 100644 --- a/plugins/woocommerce/woocommerce.php +++ b/plugins/woocommerce/woocommerce.php @@ -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(); +} diff --git a/tools/create-extension/_main.php b/tools/create-extension/_main.php index 24bb47ef30e..a7e549a653d 100644 --- a/tools/create-extension/_main.php +++ b/tools/create-extension/_main.php @@ -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; }