Merge branch 'trunk' into fix/hpos-unit-test-trait
This commit is contained in:
commit
0ef06972af
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Do not add script prop to generated block.json
|
|
@ -40,10 +40,6 @@ module.exports = {
|
||||||
wpEnv: true,
|
wpEnv: true,
|
||||||
editorScript: 'file:./index.js',
|
editorScript: 'file:./index.js',
|
||||||
editorStyle: 'file:./index.css',
|
editorStyle: 'file:./index.css',
|
||||||
style: 'file:./index.css',
|
|
||||||
customBlockJSON: {
|
|
||||||
script: 'file:./index.js',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
pluginTemplatesPath: join( __dirname, 'plugin-templates' ),
|
pluginTemplatesPath: join( __dirname, 'plugin-templates' ),
|
||||||
blockTemplatesPath: join( __dirname, 'block-templates' ),
|
blockTemplatesPath: join( __dirname, 'block-templates' ),
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add props to allow passing a classname to the feedback modal
|
|
@ -5,6 +5,7 @@ import { createElement, useState } from '@wordpress/element';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Modal } from '@wordpress/components';
|
import { Button, Modal } from '@wordpress/components';
|
||||||
import { Text } from '@woocommerce/experimental';
|
import { Text } from '@woocommerce/experimental';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a modal requesting customer feedback.
|
* Provides a modal requesting customer feedback.
|
||||||
|
@ -20,6 +21,7 @@ import { Text } from '@woocommerce/experimental';
|
||||||
* @param {string} props.cancelButtonLabel Label for the cancel button.
|
* @param {string} props.cancelButtonLabel Label for the cancel button.
|
||||||
* @param {Function} props.onModalClose Callback for when user closes modal by clicking cancel.
|
* @param {Function} props.onModalClose Callback for when user closes modal by clicking cancel.
|
||||||
* @param {Function} props.children Children to be rendered.
|
* @param {Function} props.children Children to be rendered.
|
||||||
|
* @param {string} props.className Class name to addd to the modal.
|
||||||
*/
|
*/
|
||||||
function FeedbackModal( {
|
function FeedbackModal( {
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
@ -30,6 +32,7 @@ function FeedbackModal( {
|
||||||
isSubmitButtonDisabled,
|
isSubmitButtonDisabled,
|
||||||
submitButtonLabel,
|
submitButtonLabel,
|
||||||
cancelButtonLabel,
|
cancelButtonLabel,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -39,6 +42,7 @@ function FeedbackModal( {
|
||||||
isSubmitButtonDisabled?: boolean;
|
isSubmitButtonDisabled?: boolean;
|
||||||
submitButtonLabel?: string;
|
submitButtonLabel?: string;
|
||||||
cancelButtonLabel?: string;
|
cancelButtonLabel?: string;
|
||||||
|
className?: string;
|
||||||
} ): JSX.Element | null {
|
} ): JSX.Element | null {
|
||||||
const [ isOpen, setOpen ] = useState( true );
|
const [ isOpen, setOpen ] = useState( true );
|
||||||
|
|
||||||
|
@ -55,21 +59,23 @@ function FeedbackModal( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="woocommerce-feedback-modal"
|
className={ classnames( 'woocommerce-feedback-modal', className ) }
|
||||||
title={ title }
|
title={ title }
|
||||||
onRequestClose={ closeModal }
|
onRequestClose={ closeModal }
|
||||||
shouldCloseOnClickOutside={ false }
|
shouldCloseOnClickOutside={ false }
|
||||||
>
|
>
|
||||||
<Text
|
{ description && (
|
||||||
variant="body"
|
<Text
|
||||||
as="p"
|
variant="body"
|
||||||
className="woocommerce-feedback-modal__description"
|
as="p"
|
||||||
size={ 14 }
|
className="woocommerce-feedback-modal__description"
|
||||||
lineHeight="20px"
|
size={ 14 }
|
||||||
marginBottom="1.5em"
|
lineHeight="20px"
|
||||||
>
|
marginBottom="1.5em"
|
||||||
{ description }
|
>
|
||||||
</Text>
|
{ description }
|
||||||
|
</Text>
|
||||||
|
) }
|
||||||
{ children }
|
{ children }
|
||||||
<div className="woocommerce-feedback-modal__buttons">
|
<div className="woocommerce-feedback-modal__buttons">
|
||||||
<Button isTertiary onClick={ closeModal } name="cancel">
|
<Button isTertiary onClick={ closeModal } name="cancel">
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Added install-and-activate-async to Onboarding and updated related types
|
|
@ -17,8 +17,9 @@ import {
|
||||||
TaskListType,
|
TaskListType,
|
||||||
TaskType,
|
TaskType,
|
||||||
OnboardingProductTypes,
|
OnboardingProductTypes,
|
||||||
|
InstallAndActivatePluginsAsyncResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Plugin } from '../plugins/types';
|
import { Plugin, PluginNames } from '../plugins/types';
|
||||||
|
|
||||||
export function getFreeExtensionsError( error: unknown ) {
|
export function getFreeExtensionsError( error: unknown ) {
|
||||||
return {
|
return {
|
||||||
|
@ -465,6 +466,28 @@ export function* actionTask( id: string ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* installAndActivatePluginsAsync(
|
||||||
|
plugins: Partial< PluginNames >[]
|
||||||
|
) {
|
||||||
|
yield setIsRequesting( 'installAndActivatePluginsAsync', true );
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results: InstallAndActivatePluginsAsyncResponse = yield apiFetch(
|
||||||
|
{
|
||||||
|
path: `${ WC_ADMIN_NAMESPACE }/onboarding/plugins/install-and-activate-async`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { plugins },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch ( error ) {
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
yield setIsRequesting( 'installAndActivatePluginsAsync', false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type Action = ReturnType<
|
export type Action = ReturnType<
|
||||||
| typeof getFreeExtensionsError
|
| typeof getFreeExtensionsError
|
||||||
| typeof getFreeExtensionsSuccess
|
| typeof getFreeExtensionsSuccess
|
||||||
|
|
|
@ -49,12 +49,19 @@ describe( 'plugins reducer', () => {
|
||||||
it( 'should handle SET_PROFILE_ITEMS', () => {
|
it( 'should handle SET_PROFILE_ITEMS', () => {
|
||||||
const state = reducer(
|
const state = reducer(
|
||||||
{
|
{
|
||||||
// @ts-expect-error - we're only testing profileItems
|
|
||||||
profileItems,
|
profileItems,
|
||||||
|
freeExtensions: [],
|
||||||
|
taskLists: {},
|
||||||
|
paymentMethods: [],
|
||||||
|
productTypes: {},
|
||||||
|
emailPrefill: '',
|
||||||
|
errors: {},
|
||||||
|
requesting: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: TYPES.SET_PROFILE_ITEMS,
|
type: TYPES.SET_PROFILE_ITEMS,
|
||||||
profileItems: { is_agree_marketing: true },
|
profileItems: { is_agree_marketing: true },
|
||||||
|
replace: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -64,8 +71,14 @@ describe( 'plugins reducer', () => {
|
||||||
it( 'should handle SET_PROFILE_ITEMS with replace', () => {
|
it( 'should handle SET_PROFILE_ITEMS with replace', () => {
|
||||||
const state = reducer(
|
const state = reducer(
|
||||||
{
|
{
|
||||||
// @ts-expect-error - we're only testing profileItems
|
|
||||||
profileItems,
|
profileItems,
|
||||||
|
freeExtensions: [],
|
||||||
|
taskLists: {},
|
||||||
|
paymentMethods: [],
|
||||||
|
productTypes: {},
|
||||||
|
emailPrefill: '',
|
||||||
|
errors: {},
|
||||||
|
requesting: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: TYPES.SET_PROFILE_ITEMS,
|
type: TYPES.SET_PROFILE_ITEMS,
|
||||||
|
|
|
@ -117,7 +117,7 @@ export type RevenueTypeSlug =
|
||||||
| 'more-than-250000';
|
| 'more-than-250000';
|
||||||
|
|
||||||
export type ProfileItems = {
|
export type ProfileItems = {
|
||||||
business_extensions?: [] | null;
|
business_extensions?: string[] | null;
|
||||||
completed?: boolean | null;
|
completed?: boolean | null;
|
||||||
industry?: Industry[] | null;
|
industry?: Industry[] | null;
|
||||||
number_employees?: string | null;
|
number_employees?: string | null;
|
||||||
|
@ -180,7 +180,21 @@ export type Extension = {
|
||||||
image_url: string;
|
image_url: string;
|
||||||
manage_url: string;
|
manage_url: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
label?: string;
|
||||||
is_built_by_wc: boolean;
|
is_built_by_wc: boolean;
|
||||||
is_visible: boolean;
|
is_visible: boolean;
|
||||||
is_installed?: boolean;
|
is_installed?: boolean;
|
||||||
|
is_activated?: boolean;
|
||||||
|
learn_more_link?: string;
|
||||||
|
install_priority?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InstallAndActivatePluginsAsyncResponse = {
|
||||||
|
job_id: string;
|
||||||
|
status: 'pending' | 'in-progress' | 'completed' | 'failed';
|
||||||
|
plugins: Array< {
|
||||||
|
status: 'pending' | 'installing' | 'installed' | 'activated' | 'failed';
|
||||||
|
errors: string[];
|
||||||
|
install_duration?: number;
|
||||||
|
} >;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add editor history and undo/redo toolbar buttons
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update product CES modal design and fields
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Remove editor-styles-wrapper from product editor
|
|
@ -107,16 +107,14 @@ export function BlockEditor( {
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
settings={ settings }
|
settings={ settings }
|
||||||
>
|
>
|
||||||
<div className="editor-styles-wrapper">
|
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
{ /* @ts-ignore No types for this exist yet. */ }
|
||||||
{ /* @ts-ignore No types for this exist yet. */ }
|
<BlockEditorKeyboardShortcuts.Register />
|
||||||
<BlockEditorKeyboardShortcuts.Register />
|
<BlockTools>
|
||||||
<BlockTools>
|
<ObserveTyping>
|
||||||
<ObserveTyping>
|
<BlockList className="woocommerce-product-block-editor__block-list" />
|
||||||
<BlockList className="woocommerce-product-block-editor__block-list" />
|
</ObserveTyping>
|
||||||
</ObserveTyping>
|
</BlockTools>
|
||||||
</BlockTools>
|
|
||||||
</div>
|
|
||||||
</BlockEditorProvider>
|
</BlockEditorProvider>
|
||||||
</BlockContextProvider>
|
</BlockContextProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,10 @@
|
||||||
font-family: var(--wp--preset--font-family--system-font);
|
font-family: var(--wp--preset--font-family--system-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
color: $gray-900;
|
color: $gray-900;
|
||||||
}
|
}
|
||||||
|
@ -22,6 +26,20 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Override default block margins and layout applied for themes without a theme.json file.
|
||||||
|
*
|
||||||
|
* If we no longer call `is_block_editor( true )` in the future for the product editor,
|
||||||
|
* we can remove this.
|
||||||
|
*
|
||||||
|
* See: `wp_add_editor_classic_theme_styles()`
|
||||||
|
*/
|
||||||
|
:where(.wp-block) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.has-error {
|
.has-error {
|
||||||
.components-base-control {
|
.components-base-control {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
@ -64,26 +82,6 @@
|
||||||
margin-top: calc(2 * $gap);
|
margin-top: calc(2 * $gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-styles-wrapper {
|
|
||||||
h4 {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-editor-block-list__layout.is-root-container {
|
|
||||||
padding-left: calc(2 * $gap);
|
|
||||||
padding-right: calc(2 * $gap);
|
|
||||||
padding-bottom: 128px;
|
|
||||||
|
|
||||||
@include breakpoint(">782px") {
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
max-width: 650px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.components-input-control {
|
.components-input-control {
|
||||||
&__input::placeholder {
|
&__input::placeholder {
|
||||||
color: $gray-700;
|
color: $gray-700;
|
||||||
|
@ -126,9 +124,24 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-editor-block-list__layout
|
.block-editor-block-list__layout {
|
||||||
|
&.is-root-container {
|
||||||
|
padding-left: calc(2 * $gap);
|
||||||
|
padding-right: calc(2 * $gap);
|
||||||
|
padding-bottom: 128px;
|
||||||
|
|
||||||
|
@include breakpoint(">782px") {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
max-width: 650px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.block-editor-block-list__block:not([contenteditable]):focus:after {
|
.block-editor-block-list__block:not([contenteditable]):focus:after {
|
||||||
display: none; // use important or increase specificity.
|
display: none; // use important or increase specificity.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createContext } from '@wordpress/element';
|
||||||
|
|
||||||
|
type EditorContextType = {
|
||||||
|
hasRedo: boolean;
|
||||||
|
hasUndo: boolean;
|
||||||
|
isInserterOpened: boolean;
|
||||||
|
redo: () => void;
|
||||||
|
setIsInserterOpened: ( value: boolean ) => void;
|
||||||
|
undo: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorContext = createContext< EditorContextType >( {
|
||||||
|
hasRedo: false,
|
||||||
|
hasUndo: false,
|
||||||
|
isInserterOpened: false,
|
||||||
|
redo: () => {},
|
||||||
|
setIsInserterOpened: () => {},
|
||||||
|
undo: () => {},
|
||||||
|
} );
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, isRTL } from '@wordpress/i18n';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { createElement, forwardRef, useContext } from '@wordpress/element';
|
||||||
|
import { redo as redoIcon, undo as undoIcon } from '@wordpress/icons';
|
||||||
|
import { Ref } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { EditorContext } from '../context';
|
||||||
|
|
||||||
|
function EditorHistoryRedo(
|
||||||
|
props: { [ key: string ]: unknown },
|
||||||
|
ref: Ref< HTMLButtonElement >
|
||||||
|
) {
|
||||||
|
const { hasRedo, redo } = useContext( EditorContext );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{ ...props }
|
||||||
|
ref={ ref }
|
||||||
|
icon={ ! isRTL() ? redoIcon : undoIcon }
|
||||||
|
/* translators: button label text should, if possible, be under 16 characters. */
|
||||||
|
label={ __( 'Redo', 'woocommerce' ) }
|
||||||
|
// If there are no redo levels we don't want to actually disable this
|
||||||
|
// button, because it will remove focus for keyboard users.
|
||||||
|
// See: https://github.com/WordPress/gutenberg/issues/3486
|
||||||
|
aria-disabled={ ! hasRedo }
|
||||||
|
onClick={ hasRedo ? redo : undefined }
|
||||||
|
className="editor-history__redo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef( EditorHistoryRedo );
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, isRTL } from '@wordpress/i18n';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { createElement, forwardRef, useContext } from '@wordpress/element';
|
||||||
|
import { Ref } from 'react';
|
||||||
|
import { undo as undoIcon, redo as redoIcon } from '@wordpress/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { EditorContext } from '../context';
|
||||||
|
|
||||||
|
function EditorHistoryUndo(
|
||||||
|
props: { [ key: string ]: unknown },
|
||||||
|
ref: Ref< HTMLButtonElement >
|
||||||
|
) {
|
||||||
|
const { hasUndo, undo } = useContext( EditorContext );
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{ ...props }
|
||||||
|
ref={ ref }
|
||||||
|
icon={ ! isRTL() ? undoIcon : redoIcon }
|
||||||
|
/* translators: button label text should, if possible, be under 16 characters. */
|
||||||
|
label={ __( 'Undo', 'woocommerce' ) }
|
||||||
|
// If there are no undo levels we don't want to actually disable this
|
||||||
|
// button, because it will remove focus for keyboard users.
|
||||||
|
// See: https://github.com/WordPress/gutenberg/issues/3486
|
||||||
|
aria-disabled={ ! hasUndo }
|
||||||
|
onClick={ hasUndo ? undo : undefined }
|
||||||
|
className="editor-history__undo"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef( EditorHistoryUndo );
|
|
@ -2,29 +2,37 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { useViewportMatch } from '@wordpress/compose';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
NavigableToolbar,
|
NavigableToolbar,
|
||||||
store as blockEditorStore,
|
store as blockEditorStore,
|
||||||
} from '@wordpress/block-editor';
|
} from '@wordpress/block-editor';
|
||||||
import { plus } from '@wordpress/icons';
|
import { plus } from '@wordpress/icons';
|
||||||
import { createElement, useRef, useCallback } from '@wordpress/element';
|
import {
|
||||||
|
createElement,
|
||||||
|
Fragment,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ToolbarItem exists in WordPress components.
|
// @ts-ignore ToolbarItem exists in WordPress components.
|
||||||
// eslint-disable-next-line @woocommerce/dependency-group
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
import { Button, ToolbarItem } from '@wordpress/components';
|
import { Button, ToolbarItem } from '@wordpress/components';
|
||||||
|
|
||||||
type HeaderToolbarProps = {
|
/**
|
||||||
isInserterOpened: boolean;
|
* Internal dependencies
|
||||||
setIsInserterOpened: ( value: boolean ) => void;
|
*/
|
||||||
};
|
import { EditorContext } from '../context';
|
||||||
|
import EditorHistoryRedo from './editor-history-redo';
|
||||||
|
import EditorHistoryUndo from './editor-history-undo';
|
||||||
|
|
||||||
export function HeaderToolbar( {
|
export function HeaderToolbar() {
|
||||||
isInserterOpened,
|
const { isInserterOpened, setIsInserterOpened } =
|
||||||
setIsInserterOpened,
|
useContext( EditorContext );
|
||||||
}: HeaderToolbarProps ) {
|
const isWideViewport = useViewportMatch( 'wide' );
|
||||||
// console.log( editPost );
|
|
||||||
const inserterButton = useRef< HTMLButtonElement | null >( null );
|
const inserterButton = useRef< HTMLButtonElement | null >( null );
|
||||||
const { isInserterEnabled } = useSelect( ( select ) => {
|
const { isInserterEnabled } = useSelect( ( select ) => {
|
||||||
const {
|
const {
|
||||||
|
@ -88,6 +96,12 @@ export function HeaderToolbar( {
|
||||||
}
|
}
|
||||||
showTooltip
|
showTooltip
|
||||||
/>
|
/>
|
||||||
|
{ isWideViewport && (
|
||||||
|
<>
|
||||||
|
<ToolbarItem as={ EditorHistoryUndo } />
|
||||||
|
<ToolbarItem as={ EditorHistoryRedo } />
|
||||||
|
</>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
</NavigableToolbar>
|
</NavigableToolbar>
|
||||||
);
|
);
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { BlockInstance } from '@wordpress/blocks';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
|
||||||
|
type useEditorHistoryProps = {
|
||||||
|
maxHistory?: number;
|
||||||
|
setBlocks: ( blocks: BlockInstance[] ) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MAX_HISTORY = 50;
|
||||||
|
|
||||||
|
export function useEditorHistory( {
|
||||||
|
maxHistory = DEFAULT_MAX_HISTORY,
|
||||||
|
setBlocks,
|
||||||
|
}: useEditorHistoryProps ) {
|
||||||
|
const [ edits, setEdits ] = useState< BlockInstance[][] >( [] );
|
||||||
|
const [ offsetIndex, setOffsetIndex ] = useState< number >( 0 );
|
||||||
|
|
||||||
|
function appendEdit( edit: BlockInstance[] ) {
|
||||||
|
const currentEdits = edits.slice( 0, offsetIndex + 1 );
|
||||||
|
const newEdits = [ ...currentEdits, edit ].slice( maxHistory * -1 );
|
||||||
|
setEdits( newEdits );
|
||||||
|
setOffsetIndex( newEdits.length - 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
const newIndex = Math.max( 0, offsetIndex - 1 );
|
||||||
|
if ( ! edits[ newIndex ] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBlocks( edits[ newIndex ] );
|
||||||
|
setOffsetIndex( newIndex );
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
const newIndex = Math.min( edits.length - 1, offsetIndex + 1 );
|
||||||
|
if ( ! edits[ newIndex ] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBlocks( edits[ newIndex ] );
|
||||||
|
setOffsetIndex( newIndex );
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasUndo() {
|
||||||
|
return !! edits.length && offsetIndex > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRedo() {
|
||||||
|
return !! edits.length && offsetIndex < edits.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
appendEdit,
|
||||||
|
hasRedo: hasRedo(),
|
||||||
|
hasUndo: hasUndo(),
|
||||||
|
redo,
|
||||||
|
undo,
|
||||||
|
};
|
||||||
|
}
|
|
@ -26,9 +26,11 @@ import {
|
||||||
*/
|
*/
|
||||||
import { BackButton } from './back-button';
|
import { BackButton } from './back-button';
|
||||||
import { EditorCanvas } from './editor-canvas';
|
import { EditorCanvas } from './editor-canvas';
|
||||||
import { HeaderToolbar } from './header-toolbar';
|
import { EditorContext } from './context';
|
||||||
|
import { HeaderToolbar } from './header-toolbar/header-toolbar';
|
||||||
import { ResizableEditor } from './resizable-editor';
|
import { ResizableEditor } from './resizable-editor';
|
||||||
import { SecondarySidebar } from './secondary-sidebar/secondary-sidebar';
|
import { SecondarySidebar } from './secondary-sidebar/secondary-sidebar';
|
||||||
|
import { useEditorHistory } from './hooks/use-editor-history';
|
||||||
|
|
||||||
type IframeEditorProps = {
|
type IframeEditorProps = {
|
||||||
initialBlocks?: BlockInstance[];
|
initialBlocks?: BlockInstance[];
|
||||||
|
@ -47,6 +49,9 @@ export function IframeEditor( {
|
||||||
}: IframeEditorProps ) {
|
}: IframeEditorProps ) {
|
||||||
const [ resizeObserver, sizes ] = useResizeObserver();
|
const [ resizeObserver, sizes ] = useResizeObserver();
|
||||||
const [ blocks, setBlocks ] = useState< BlockInstance[] >( initialBlocks );
|
const [ blocks, setBlocks ] = useState< BlockInstance[] >( initialBlocks );
|
||||||
|
const { appendEdit, hasRedo, hasUndo, redo, undo } = useEditorHistory( {
|
||||||
|
setBlocks,
|
||||||
|
} );
|
||||||
const [ isInserterOpened, setIsInserterOpened ] = useState( false );
|
const [ isInserterOpened, setIsInserterOpened ] = useState( false );
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore This action exists in the block editor store.
|
// @ts-ignore This action exists in the block editor store.
|
||||||
|
@ -70,74 +75,80 @@ export function IframeEditor( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-iframe-editor">
|
<div className="woocommerce-iframe-editor">
|
||||||
<BlockEditorProvider
|
<EditorContext.Provider
|
||||||
settings={ {
|
value={ {
|
||||||
...settings,
|
hasRedo,
|
||||||
hasFixedToolbar: true,
|
hasUndo,
|
||||||
templateLock: false,
|
isInserterOpened,
|
||||||
|
redo,
|
||||||
|
setIsInserterOpened,
|
||||||
|
undo,
|
||||||
} }
|
} }
|
||||||
value={ blocks }
|
|
||||||
onChange={ ( updatedBlocks: BlockInstance[] ) => {
|
|
||||||
setBlocks( updatedBlocks );
|
|
||||||
onChange( updatedBlocks );
|
|
||||||
} }
|
|
||||||
onInput={ onInput }
|
|
||||||
useSubRegistry={ true }
|
|
||||||
>
|
>
|
||||||
<HeaderToolbar
|
<BlockEditorProvider
|
||||||
isInserterOpened={ isInserterOpened }
|
settings={ {
|
||||||
setIsInserterOpened={ setIsInserterOpened }
|
...settings,
|
||||||
/>
|
hasFixedToolbar: true,
|
||||||
<div className="woocommerce-iframe-editor__main">
|
templateLock: false,
|
||||||
<SecondarySidebar
|
} }
|
||||||
isInserterOpened={ isInserterOpened }
|
value={ blocks }
|
||||||
setIsInserterOpened={ setIsInserterOpened }
|
onChange={ ( updatedBlocks: BlockInstance[] ) => {
|
||||||
/>
|
appendEdit( updatedBlocks );
|
||||||
<BlockTools
|
setBlocks( updatedBlocks );
|
||||||
className={ 'woocommerce-iframe-editor__content' }
|
onChange( updatedBlocks );
|
||||||
onClick={ (
|
} }
|
||||||
event: React.MouseEvent<
|
onInput={ onInput }
|
||||||
HTMLDivElement,
|
useSubRegistry={ true }
|
||||||
MouseEvent
|
>
|
||||||
>
|
<HeaderToolbar />
|
||||||
) => {
|
<div className="woocommerce-iframe-editor__main">
|
||||||
// Clear selected block when clicking on the gray background.
|
<SecondarySidebar />
|
||||||
if ( event.target === event.currentTarget ) {
|
<BlockTools
|
||||||
clearSelectedBlock();
|
className={ 'woocommerce-iframe-editor__content' }
|
||||||
}
|
onClick={ (
|
||||||
} }
|
event: React.MouseEvent<
|
||||||
>
|
HTMLDivElement,
|
||||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
MouseEvent
|
||||||
{ /* @ts-ignore */ }
|
>
|
||||||
<BlockEditorKeyboardShortcuts.Register />
|
) => {
|
||||||
{ onClose && (
|
// Clear selected block when clicking on the gray background.
|
||||||
<BackButton
|
if ( event.target === event.currentTarget ) {
|
||||||
onClick={ () => {
|
clearSelectedBlock();
|
||||||
setTimeout( onClose, 550 );
|
}
|
||||||
} }
|
} }
|
||||||
/>
|
|
||||||
) }
|
|
||||||
<ResizableEditor
|
|
||||||
enableResizing={ true }
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore This accepts numbers or strings.
|
|
||||||
height={ sizes.height ?? '100%' }
|
|
||||||
>
|
>
|
||||||
<EditorCanvas
|
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||||
|
{ /* @ts-ignore */ }
|
||||||
|
<BlockEditorKeyboardShortcuts.Register />
|
||||||
|
{ onClose && (
|
||||||
|
<BackButton
|
||||||
|
onClick={ () => {
|
||||||
|
setTimeout( onClose, 550 );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<ResizableEditor
|
||||||
enableResizing={ true }
|
enableResizing={ true }
|
||||||
settings={ settings }
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore This accepts numbers or strings.
|
||||||
|
height={ sizes.height ?? '100%' }
|
||||||
>
|
>
|
||||||
{ resizeObserver }
|
<EditorCanvas
|
||||||
<BlockList className="edit-site-block-editor__block-list wp-site-blocks" />
|
enableResizing={ true }
|
||||||
</EditorCanvas>
|
settings={ settings }
|
||||||
<Popover.Slot />
|
>
|
||||||
</ResizableEditor>
|
{ resizeObserver }
|
||||||
</BlockTools>
|
<BlockList className="edit-site-block-editor__block-list wp-site-blocks" />
|
||||||
<div className="woocommerce-iframe-editor__sidebar">
|
</EditorCanvas>
|
||||||
<BlockInspector />
|
<Popover.Slot />
|
||||||
|
</ResizableEditor>
|
||||||
|
</BlockTools>
|
||||||
|
<div className="woocommerce-iframe-editor__sidebar">
|
||||||
|
<BlockInspector />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BlockEditorProvider>
|
||||||
</BlockEditorProvider>
|
</EditorContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import {
|
import {
|
||||||
createElement,
|
createElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
|
@ -23,13 +24,13 @@ import {
|
||||||
__experimentalLibrary as Library,
|
__experimentalLibrary as Library,
|
||||||
} from '@wordpress/block-editor';
|
} from '@wordpress/block-editor';
|
||||||
|
|
||||||
type InserterSidebarProps = {
|
/**
|
||||||
setIsInserterOpened: ( value: boolean ) => void;
|
* Internal dependencies
|
||||||
};
|
*/
|
||||||
|
import { EditorContext } from '../context';
|
||||||
|
|
||||||
export default function InserterSidebar( {
|
export default function InserterSidebar() {
|
||||||
setIsInserterOpened,
|
const { setIsInserterOpened } = useContext( EditorContext );
|
||||||
}: InserterSidebarProps ) {
|
|
||||||
const isMobileViewport = useViewportMatch( 'medium', '<' );
|
const isMobileViewport = useViewportMatch( 'medium', '<' );
|
||||||
const { rootClientId } = useSelect( ( select ) => {
|
const { rootClientId } = useSelect( ( select ) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement } from '@wordpress/element';
|
import { createElement, useContext } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { EditorContext } from '../context';
|
||||||
import InserterSidebar from './inserter-sidebar';
|
import InserterSidebar from './inserter-sidebar';
|
||||||
|
|
||||||
type SecondarySidebarProps = {
|
export function SecondarySidebar() {
|
||||||
isInserterOpened: boolean;
|
const { isInserterOpened } = useContext( EditorContext );
|
||||||
setIsInserterOpened: ( value: boolean ) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SecondarySidebar( {
|
|
||||||
isInserterOpened,
|
|
||||||
setIsInserterOpened,
|
|
||||||
}: SecondarySidebarProps ) {
|
|
||||||
if ( isInserterOpened ) {
|
if ( isInserterOpened ) {
|
||||||
return <InserterSidebar setIsInserterOpened={ setIsInserterOpened } />;
|
return <InserterSidebar />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
@import './iframe-editor.scss';
|
@import './iframe-editor.scss';
|
||||||
@import './header-toolbar.scss';
|
@import './header-toolbar/header-toolbar.scss';
|
||||||
@import './resize-handle.scss';
|
@import './resize-handle.scss';
|
||||||
@import './back-button.scss';
|
@import './back-button.scss';
|
||||||
@import './secondary-sidebar/inserter-sidebar.scss';
|
@import './secondary-sidebar/inserter-sidebar.scss';
|
||||||
|
|
|
@ -41,11 +41,16 @@ export const ProductMVPFeedbackModalContainer: React.FC< {
|
||||||
`post-new.php?post_type=product&product_block_editor=0&_feature_nonce=${ _feature_nonce }`
|
`post-new.php?post_type=product&product_block_editor=0&_feature_nonce=${ _feature_nonce }`
|
||||||
);
|
);
|
||||||
|
|
||||||
const recordScore = ( checked: string[], comments: string ) => {
|
const recordScore = (
|
||||||
|
checked: string[],
|
||||||
|
comments: string,
|
||||||
|
email: string
|
||||||
|
) => {
|
||||||
recordEvent( 'product_mvp_feedback', {
|
recordEvent( 'product_mvp_feedback', {
|
||||||
action: 'disable',
|
action: 'disable',
|
||||||
checked,
|
checked,
|
||||||
comments: comments || '',
|
comments: comments || '',
|
||||||
|
email,
|
||||||
} );
|
} );
|
||||||
hideProductMVPFeedbackModal();
|
hideProductMVPFeedbackModal();
|
||||||
window.location.href = `${ classicEditorUrl }&new-product-experience-disabled=true`;
|
window.location.href = `${ classicEditorUrl }&new-product-experience-disabled=true`;
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement, Fragment, useState } from '@wordpress/element';
|
import {
|
||||||
|
createElement,
|
||||||
|
createInterpolateElement,
|
||||||
|
Fragment,
|
||||||
|
useState,
|
||||||
|
} from '@wordpress/element';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CheckboxControl, TextareaControl } from '@wordpress/components';
|
import {
|
||||||
|
CheckboxControl,
|
||||||
|
TextareaControl,
|
||||||
|
TextControl,
|
||||||
|
} from '@wordpress/components';
|
||||||
import { FeedbackModal } from '@woocommerce/customer-effort-score';
|
import { FeedbackModal } from '@woocommerce/customer-effort-score';
|
||||||
import { Text } from '@woocommerce/experimental';
|
import { Text } from '@woocommerce/experimental';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
@ -20,7 +29,11 @@ function ProductMVPFeedbackModal( {
|
||||||
recordScoreCallback,
|
recordScoreCallback,
|
||||||
onCloseModal,
|
onCloseModal,
|
||||||
}: {
|
}: {
|
||||||
recordScoreCallback: ( checked: string[], comments: string ) => void;
|
recordScoreCallback: (
|
||||||
|
checked: string[],
|
||||||
|
comments: string,
|
||||||
|
email: string
|
||||||
|
) => void;
|
||||||
onCloseModal?: () => void;
|
onCloseModal?: () => void;
|
||||||
} ): JSX.Element | null {
|
} ): JSX.Element | null {
|
||||||
const [ missingFeatures, setMissingFeatures ] = useState( false );
|
const [ missingFeatures, setMissingFeatures ] = useState( false );
|
||||||
|
@ -61,37 +74,33 @@ function ProductMVPFeedbackModal( {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const [ comments, setComments ] = useState( '' );
|
const [ comments, setComments ] = useState( '' );
|
||||||
|
const [ email, setEmail ] = useState( '' );
|
||||||
|
const checked = checkboxes
|
||||||
|
.filter( ( checkbox ) => checkbox.checked )
|
||||||
|
.map( ( checkbox ) => checkbox.key );
|
||||||
|
|
||||||
const onSendFeedback = () => {
|
const onSendFeedback = () => {
|
||||||
const checked = checkboxes
|
recordScoreCallback( checked, comments, email );
|
||||||
.filter( ( checkbox ) => checkbox.checked )
|
|
||||||
.map( ( checkbox ) => checkbox.key );
|
|
||||||
recordScoreCallback( checked, comments );
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSendButtonDisabled =
|
const optionalElement = (
|
||||||
! comments &&
|
<span className="woocommerce-product-mvp-feedback-modal__optional">
|
||||||
! missingFeatures &&
|
{ __( '(optional)', 'woocommerce' ) }
|
||||||
! missingPlugins &&
|
</span>
|
||||||
! difficultToUse &&
|
);
|
||||||
! slowBuggyOrBroken &&
|
|
||||||
! other;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeedbackModal
|
<FeedbackModal
|
||||||
title={ __(
|
title={ __(
|
||||||
'Thanks for trying out the new product editor!',
|
'Thanks for trying out the new product form!',
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
description={ __(
|
|
||||||
'We’re working on making it better, and your feedback will help improve the experience for thousands of merchants like you.',
|
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
) }
|
) }
|
||||||
onSubmit={ onSendFeedback }
|
onSubmit={ onSendFeedback }
|
||||||
onModalClose={ onCloseModal }
|
onModalClose={ onCloseModal }
|
||||||
isSubmitButtonDisabled={ isSendButtonDisabled }
|
isSubmitButtonDisabled={ ! checked.length }
|
||||||
submitButtonLabel={ __( 'Send feedback', 'woocommerce' ) }
|
submitButtonLabel={ __( 'Send feedback', 'woocommerce' ) }
|
||||||
cancelButtonLabel={ __( 'Skip', 'woocommerce' ) }
|
cancelButtonLabel={ __( 'Skip', 'woocommerce' ) }
|
||||||
|
className="woocommerce-product-mvp-feedback-modal"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
|
@ -100,45 +109,63 @@ function ProductMVPFeedbackModal( {
|
||||||
weight="600"
|
weight="600"
|
||||||
size="14"
|
size="14"
|
||||||
lineHeight="20px"
|
lineHeight="20px"
|
||||||
>
|
></Text>
|
||||||
{ __(
|
<fieldset className="woocommerce-product-mvp-feedback-modal__reason">
|
||||||
'What made you switch back to the classic product editor?',
|
<legend>
|
||||||
'woocommerce'
|
{ __(
|
||||||
) }
|
'What made you turn off the new product form?',
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
weight="400"
|
|
||||||
size="12"
|
|
||||||
as="p"
|
|
||||||
lineHeight="16px"
|
|
||||||
color="#757575"
|
|
||||||
className="woocommerce-product-mvp-feedback-modal__subtitle"
|
|
||||||
>
|
|
||||||
{ __( '(Check all that apply)', 'woocommerce' ) }
|
|
||||||
</Text>
|
|
||||||
<div className="woocommerce-product-mvp-feedback-modal__checkboxes">
|
|
||||||
{ checkboxes.map( ( checkbox, index ) => (
|
|
||||||
<CheckboxControl
|
|
||||||
key={ index }
|
|
||||||
label={ checkbox.label }
|
|
||||||
name={ checkbox.key }
|
|
||||||
checked={ checkbox.checked }
|
|
||||||
onChange={ checkbox.onChange }
|
|
||||||
/>
|
|
||||||
) ) }
|
|
||||||
</div>
|
|
||||||
<div className="woocommerce-product-mvp-feedback-modal__comments">
|
|
||||||
<TextareaControl
|
|
||||||
label={ __( 'Additional comments', 'woocommerce' ) }
|
|
||||||
value={ comments }
|
|
||||||
placeholder={ __(
|
|
||||||
'Optional, but much apprecated. We love reading your feedback!',
|
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
) }
|
) }
|
||||||
|
</legend>
|
||||||
|
<div className="woocommerce-product-mvp-feedback-modal__checkboxes">
|
||||||
|
{ checkboxes.map( ( checkbox, index ) => (
|
||||||
|
<CheckboxControl
|
||||||
|
key={ index }
|
||||||
|
label={ checkbox.label }
|
||||||
|
name={ checkbox.key }
|
||||||
|
checked={ checkbox.checked }
|
||||||
|
onChange={ checkbox.onChange }
|
||||||
|
/>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div className="woocommerce-product-mvp-feedback-modal__comments">
|
||||||
|
<TextareaControl
|
||||||
|
label={ createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Additional thoughts <optional/>',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
optional: optionalElement,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
value={ comments }
|
||||||
onChange={ ( value: string ) => setComments( value ) }
|
onChange={ ( value: string ) => setComments( value ) }
|
||||||
rows={ 5 }
|
rows={ 5 }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="woocommerce-product-mvp-feedback-modal__email">
|
||||||
|
<TextControl
|
||||||
|
label={ createInterpolateElement(
|
||||||
|
__(
|
||||||
|
'Your email address <optional/>',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
optional: optionalElement,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
value={ email }
|
||||||
|
onChange={ ( value: string ) => setEmail( value ) }
|
||||||
|
rows={ 5 }
|
||||||
|
help={ __(
|
||||||
|
'In case you want to participate in further discussion and future user research.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
</FeedbackModal>
|
</FeedbackModal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,23 +1,62 @@
|
||||||
|
$modal-header-height: 84px;
|
||||||
|
|
||||||
.woocommerce-product-mvp-feedback-modal {
|
.woocommerce-product-mvp-feedback-modal {
|
||||||
|
width: 600px;
|
||||||
|
|
||||||
|
.components-modal__header {
|
||||||
|
height: $modal-header-height;
|
||||||
|
|
||||||
|
&-heading {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-modal__content {
|
||||||
|
margin-top: $modal-header-height;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend,
|
||||||
|
label {
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-product-mvp-feedback-modal__optional {
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: $gap-small;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&__subtitle {
|
&__subtitle {
|
||||||
margin-top: $gap-smaller !important;
|
margin-top: $gap-smaller !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__checkboxes {
|
&__checkboxes {
|
||||||
margin: $gap-small 0;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__comments {
|
&__comments {
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
margin-bottom: 1.5em;
|
margin-bottom: 1.5em;
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: none;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__reason,
|
||||||
|
&__comments,
|
||||||
|
&__email {
|
||||||
|
margin-bottom: $gap-large;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ describe( 'ProductMVPFeedbackModal', () => {
|
||||||
fireEvent.click( screen.getByRole( 'checkbox', { name: /other/i } ) );
|
fireEvent.click( screen.getByRole( 'checkbox', { name: /other/i } ) );
|
||||||
expect( mockRecordScoreCallback ).toHaveBeenCalledWith(
|
expect( mockRecordScoreCallback ).toHaveBeenCalledWith(
|
||||||
[ 'other' ],
|
[ 'other' ],
|
||||||
|
'',
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
CoreProfilerStateMachineContext,
|
CoreProfilerStateMachineContext,
|
||||||
UserProfileEvent,
|
UserProfileEvent,
|
||||||
BusinessInfoEvent,
|
BusinessInfoEvent,
|
||||||
|
PluginsLearnMoreLinkClicked,
|
||||||
} from '..';
|
} from '..';
|
||||||
import { POSSIBLY_DEFAULT_STORE_NAMES } from '../pages/BusinessInfo';
|
import { POSSIBLY_DEFAULT_STORE_NAMES } from '../pages/BusinessInfo';
|
||||||
|
|
||||||
|
@ -89,6 +90,18 @@ const recordTracksBusinessInfoCompleted = (
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recordTracksPluginsLearnMoreLinkClicked = (
|
||||||
|
_context: unknown,
|
||||||
|
_event: PluginsLearnMoreLinkClicked,
|
||||||
|
{ action }: { action: unknown }
|
||||||
|
) => {
|
||||||
|
const { step } = action as { step: string };
|
||||||
|
recordEvent( `storeprofiler_${ step }_learn_more_link_clicked`, {
|
||||||
|
plugin: _event.payload.plugin,
|
||||||
|
link: _event.payload.learnMoreLink,
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
recordTracksStepViewed,
|
recordTracksStepViewed,
|
||||||
recordTracksStepSkipped,
|
recordTracksStepSkipped,
|
||||||
|
@ -96,4 +109,5 @@ export default {
|
||||||
recordTracksUserProfileCompleted,
|
recordTracksUserProfileCompleted,
|
||||||
recordTracksSkipBusinessLocationCompleted,
|
recordTracksSkipBusinessLocationCompleted,
|
||||||
recordTracksBusinessInfoCompleted,
|
recordTracksBusinessInfoCompleted,
|
||||||
|
recordTracksPluginsLearnMoreLinkClicked,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
.woocommerce-profiler-heading__title {
|
.woocommerce-profiler-heading__title {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 32px;
|
font-size: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #000;
|
color: $gray-900;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $gray-700;
|
color: $gray-800;
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
color: $gray-800;
|
color: $gray-800;
|
||||||
|
@ -50,8 +50,6 @@
|
||||||
margin: 52px 0 40px;
|
margin: 52px 0 40px;
|
||||||
|
|
||||||
.woocommerce-profiler-heading__title {
|
.woocommerce-profiler-heading__title {
|
||||||
font-size: 32px;
|
|
||||||
line-height: 40px;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.woocommerce-profiler-heading__subtitle {
|
.woocommerce-profiler-heading__subtitle {
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.components-checkbox-control__input-container {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
margin: 3px 26px 0 0;
|
margin: 3px 26px 0 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@ -26,8 +30,7 @@
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
width: 25px;
|
width: 28px;
|
||||||
height: 25px;
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
@ -35,6 +38,7 @@
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
font-weight: 500;
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
|
@ -43,14 +47,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
display: inline;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: $gray-700;
|
color: $gray-700;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
a {
|
@include breakpoint( '<782px' ) {
|
||||||
color: inherit;
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $gray-700;
|
||||||
|
margin-left: 5px;
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -63,6 +73,7 @@
|
||||||
height: 16px;
|
height: 16px;
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
border-color: #1e1e1e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.components-checkbox-control__input[type='checkbox']:checked {
|
.components-checkbox-control__input[type='checkbox']:checked {
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const PluginCard = ( {
|
||||||
onChange,
|
onChange,
|
||||||
checked = false,
|
checked = false,
|
||||||
description,
|
description,
|
||||||
|
learnMoreLink,
|
||||||
}: {
|
}: {
|
||||||
// Checkbox will be hidden if true
|
// Checkbox will be hidden if true
|
||||||
installed?: boolean;
|
installed?: boolean;
|
||||||
|
@ -28,6 +29,7 @@ export const PluginCard = ( {
|
||||||
description: string | ReactNode;
|
description: string | ReactNode;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
|
learnMoreLink?: ReactNode;
|
||||||
} ) => {
|
} ) => {
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-profiler-plugins-plugin-card">
|
<div className="woocommerce-profiler-plugins-plugin-card">
|
||||||
|
@ -53,6 +55,7 @@ export const PluginCard = ( {
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
<p dangerouslySetInnerHTML={ sanitizeHTML( description ) } />
|
<p dangerouslySetInnerHTML={ sanitizeHTML( description ) } />
|
||||||
|
{ learnMoreLink }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -106,6 +106,14 @@ export type PluginsInstallationRequestedEvent = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PluginsLearnMoreLinkClicked = {
|
||||||
|
type: 'PLUGINS_LEARN_MORE_LINK_CLICKED';
|
||||||
|
payload: {
|
||||||
|
plugin: string;
|
||||||
|
learnMoreLink: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: add types as we develop the pages
|
// TODO: add types as we develop the pages
|
||||||
export type OnboardingProfile = {
|
export type OnboardingProfile = {
|
||||||
business_choice: BusinessChoice;
|
business_choice: BusinessChoice;
|
||||||
|
@ -1107,6 +1115,14 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
||||||
],
|
],
|
||||||
target: 'pluginsSkipped',
|
target: 'pluginsSkipped',
|
||||||
},
|
},
|
||||||
|
PLUGINS_LEARN_MORE_LINK_CLICKED: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'recordTracksPluginsLearnMoreLinkClicked',
|
||||||
|
step: 'plugins',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
PLUGINS_INSTALLATION_REQUESTED: {
|
PLUGINS_INSTALLATION_REQUESTED: {
|
||||||
target: 'installPlugins',
|
target: 'installPlugins',
|
||||||
actions: [ 'assignPluginsSelected' ],
|
actions: [ 'assignPluginsSelected' ],
|
||||||
|
@ -1266,6 +1282,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
|
||||||
data: ( context ) => {
|
data: ( context ) => {
|
||||||
return {
|
return {
|
||||||
selectedPlugins: context.pluginsSelected,
|
selectedPlugins: context.pluginsSelected,
|
||||||
|
pluginsAvailable: context.pluginsAvailable,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,15 +17,13 @@ import { Navigation } from '../components/navigation/navigation';
|
||||||
export const IntroOptIn = ( {
|
export const IntroOptIn = ( {
|
||||||
sendEvent,
|
sendEvent,
|
||||||
navigationProgress,
|
navigationProgress,
|
||||||
context,
|
|
||||||
}: {
|
}: {
|
||||||
sendEvent: ( event: IntroOptInEvent ) => void;
|
sendEvent: ( event: IntroOptInEvent ) => void;
|
||||||
navigationProgress: number;
|
navigationProgress: number;
|
||||||
context: CoreProfilerStateMachineContext;
|
context: CoreProfilerStateMachineContext;
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ iOptInDataSharing, setIsOptInDataSharing ] = useState< boolean >(
|
const [ iOptInDataSharing, setIsOptInDataSharing ] =
|
||||||
context.optInDataSharing
|
useState< boolean >( true );
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -11,7 +11,10 @@ import { useState } from 'react';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { CoreProfilerStateMachineContext } from '../index';
|
import {
|
||||||
|
CoreProfilerStateMachineContext,
|
||||||
|
PluginsLearnMoreLinkClicked,
|
||||||
|
} from '../index';
|
||||||
import { PluginsInstallationRequestedEvent, PluginsPageSkippedEvent } from '..';
|
import { PluginsInstallationRequestedEvent, PluginsPageSkippedEvent } from '..';
|
||||||
import { Heading } from '../components/heading/heading';
|
import { Heading } from '../components/heading/heading';
|
||||||
import { Navigation } from '../components/navigation/navigation';
|
import { Navigation } from '../components/navigation/navigation';
|
||||||
|
@ -36,13 +39,16 @@ export const Plugins = ( {
|
||||||
}: {
|
}: {
|
||||||
context: CoreProfilerStateMachineContext;
|
context: CoreProfilerStateMachineContext;
|
||||||
sendEvent: (
|
sendEvent: (
|
||||||
payload: PluginsInstallationRequestedEvent | PluginsPageSkippedEvent
|
payload:
|
||||||
|
| PluginsInstallationRequestedEvent
|
||||||
|
| PluginsPageSkippedEvent
|
||||||
|
| PluginsLearnMoreLinkClicked
|
||||||
) => void;
|
) => void;
|
||||||
navigationProgress: number;
|
navigationProgress: number;
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ selectedPlugins, setSelectedPlugins ] = useState<
|
const [ selectedPlugins, setSelectedPlugins ] = useState<
|
||||||
ExtensionList[ 'plugins' ]
|
ExtensionList[ 'plugins' ]
|
||||||
>( context.pluginsAvailable.filter( ( plugin ) => ! plugin.is_installed ) );
|
>( context.pluginsAvailable.filter( ( plugin ) => ! plugin.is_activated ) );
|
||||||
|
|
||||||
const setSelectedPlugin = ( plugin: Extension ) => {
|
const setSelectedPlugin = ( plugin: Extension ) => {
|
||||||
setSelectedPlugins(
|
setSelectedPlugins(
|
||||||
|
@ -136,10 +142,29 @@ export const Plugins = ( {
|
||||||
) }
|
) }
|
||||||
<div className="woocommerce-profiler-plugins__list">
|
<div className="woocommerce-profiler-plugins__list">
|
||||||
{ context.pluginsAvailable.map( ( plugin ) => {
|
{ context.pluginsAvailable.map( ( plugin ) => {
|
||||||
|
const learnMoreLink = plugin.learn_more_link ? (
|
||||||
|
<Link
|
||||||
|
onClick={ () => {
|
||||||
|
sendEvent( {
|
||||||
|
type: 'PLUGINS_LEARN_MORE_LINK_CLICKED',
|
||||||
|
payload: {
|
||||||
|
plugin: plugin.key,
|
||||||
|
learnMoreLink:
|
||||||
|
plugin.learn_more_link ?? '',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} }
|
||||||
|
href={ plugin.learn_more_link }
|
||||||
|
target="_blank"
|
||||||
|
type="external"
|
||||||
|
>
|
||||||
|
{ __( 'Learn More', 'woocommerce' ) }
|
||||||
|
</Link>
|
||||||
|
) : null;
|
||||||
return (
|
return (
|
||||||
<PluginCard
|
<PluginCard
|
||||||
key={ `checkbox-control-${ plugin.key }` }
|
key={ `checkbox-control-${ plugin.key }` }
|
||||||
installed={ plugin.is_installed }
|
installed={ plugin.is_activated }
|
||||||
onChange={ () => {
|
onChange={ () => {
|
||||||
setSelectedPlugin( plugin );
|
setSelectedPlugin( plugin );
|
||||||
} }
|
} }
|
||||||
|
@ -156,8 +181,9 @@ export const Plugins = ( {
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
title={ plugin.name }
|
title={ plugin.label }
|
||||||
description={ plugin.description }
|
description={ plugin.description }
|
||||||
|
learnMoreLink={ learnMoreLink }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} ) }
|
} ) }
|
||||||
|
|
|
@ -227,7 +227,7 @@ export const UserProfile = ( {
|
||||||
>
|
>
|
||||||
<Navigation
|
<Navigation
|
||||||
percentage={ navigationProgress }
|
percentage={ navigationProgress }
|
||||||
skipText={ __( 'Skip this setup', 'woocommerce' ) }
|
skipText={ __( 'Skip this step', 'woocommerce' ) }
|
||||||
onSkip={ () =>
|
onSkip={ () =>
|
||||||
sendEvent( {
|
sendEvent( {
|
||||||
type: 'USER_PROFILE_SKIPPED',
|
type: 'USER_PROFILE_SKIPPED',
|
||||||
|
|
|
@ -48,20 +48,6 @@ describe( 'IntroOptIn', () => {
|
||||||
expect( screen.getByRole( 'checkbox' ) ).toBeChecked();
|
expect( screen.getByRole( 'checkbox' ) ).toBeChecked();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should checkbox be unchecked when optInDataSharing is false', () => {
|
|
||||||
const newProps = {
|
|
||||||
...props,
|
|
||||||
context: {
|
|
||||||
optInDataSharing: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
render(
|
|
||||||
// @ts-ignore
|
|
||||||
<IntroOptIn { ...newProps } />
|
|
||||||
);
|
|
||||||
expect( screen.getByRole( 'checkbox' ) ).not.toBeChecked();
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'should toggle checkbox when checkbox is clicked', () => {
|
it( 'should toggle checkbox when checkbox is clicked', () => {
|
||||||
render(
|
render(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -114,7 +114,7 @@ describe( 'UserProfile', () => {
|
||||||
);
|
);
|
||||||
screen
|
screen
|
||||||
.getByRole( 'button', {
|
.getByRole( 'button', {
|
||||||
name: /Skip this setup/i,
|
name: /Skip this step/i,
|
||||||
} )
|
} )
|
||||||
.click();
|
.click();
|
||||||
expect( props.sendEvent ).toHaveBeenCalledWith( {
|
expect( props.sendEvent ).toHaveBeenCalledWith( {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { PLUGINS_STORE_NAME, PluginNames } from '@woocommerce/data';
|
import {
|
||||||
|
ExtensionList,
|
||||||
|
ONBOARDING_STORE_NAME,
|
||||||
|
PLUGINS_STORE_NAME,
|
||||||
|
PluginNames,
|
||||||
|
} from '@woocommerce/data';
|
||||||
import { dispatch } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
import {
|
import {
|
||||||
assign,
|
assign,
|
||||||
|
@ -68,6 +73,7 @@ const createPluginInstalledAndActivatedEvent = (
|
||||||
|
|
||||||
export type PluginInstallerMachineContext = {
|
export type PluginInstallerMachineContext = {
|
||||||
selectedPlugins: PluginNames[];
|
selectedPlugins: PluginNames[];
|
||||||
|
pluginsAvailable: ExtensionList[ 'plugins' ] | [];
|
||||||
pluginsInstallationQueue: PluginNames[];
|
pluginsInstallationQueue: PluginNames[];
|
||||||
installedPlugins: InstalledPlugin[];
|
installedPlugins: InstalledPlugin[];
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
@ -93,6 +99,7 @@ export const pluginInstallerMachine = createMachine(
|
||||||
initial: 'installing',
|
initial: 'installing',
|
||||||
context: {
|
context: {
|
||||||
selectedPlugins: [] as PluginNames[],
|
selectedPlugins: [] as PluginNames[],
|
||||||
|
pluginsAvailable: [] as ExtensionList[ 'plugins' ] | [],
|
||||||
pluginsInstallationQueue: [] as PluginNames[],
|
pluginsInstallationQueue: [] as PluginNames[],
|
||||||
installedPlugins: [] as InstalledPlugin[],
|
installedPlugins: [] as InstalledPlugin[],
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
|
@ -160,7 +167,7 @@ export const pluginInstallerMachine = createMachine(
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'queueRemainingPluginsAsync',
|
src: 'queueRemainingPluginsAsync',
|
||||||
onDone: {
|
onDone: {
|
||||||
target: 'finished',
|
target: 'reportSuccess',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -188,7 +195,21 @@ export const pluginInstallerMachine = createMachine(
|
||||||
} ),
|
} ),
|
||||||
assignPluginsInstallationQueue: assign( {
|
assignPluginsInstallationQueue: assign( {
|
||||||
pluginsInstallationQueue: ( ctx ) => {
|
pluginsInstallationQueue: ( ctx ) => {
|
||||||
return ctx.selectedPlugins;
|
// Sort the plugins by install_priority so that the smaller plugins are installed first
|
||||||
|
// install_priority is set by plugin's size
|
||||||
|
// Lower install_prioirty means the plugin is smaller
|
||||||
|
return ctx.selectedPlugins.slice().sort( ( a, b ) => {
|
||||||
|
const aIndex = ctx.pluginsAvailable.find(
|
||||||
|
( plugin ) => plugin.key === a
|
||||||
|
);
|
||||||
|
const bIndex = ctx.pluginsAvailable.find(
|
||||||
|
( plugin ) => plugin.key === b
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
( aIndex?.install_priority ?? 99 ) -
|
||||||
|
( bIndex?.install_priority ?? 99 )
|
||||||
|
);
|
||||||
|
} );
|
||||||
},
|
},
|
||||||
} ),
|
} ),
|
||||||
assignStartTime: assign( {
|
assignStartTime: assign( {
|
||||||
|
@ -262,9 +283,10 @@ export const pluginInstallerMachine = createMachine(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
queueRemainingPluginsAsync: ( ctx ) => {
|
queueRemainingPluginsAsync: ( ctx ) => {
|
||||||
return dispatch( PLUGINS_STORE_NAME ).installPlugins(
|
return dispatch(
|
||||||
ctx.pluginsInstallationQueue,
|
ONBOARDING_STORE_NAME
|
||||||
true
|
).installAndActivatePluginsAsync(
|
||||||
|
ctx.pluginsInstallationQueue
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -48,6 +48,7 @@ describe( 'pluginInstallerMachine', () => {
|
||||||
.withContext( {
|
.withContext( {
|
||||||
...defaultContext,
|
...defaultContext,
|
||||||
selectedPlugins: [ 'woocommerce-payments' ],
|
selectedPlugins: [ 'woocommerce-payments' ],
|
||||||
|
pluginsAvailable: [],
|
||||||
} );
|
} );
|
||||||
|
|
||||||
dispatchInstallPluginMock.mockImplementationOnce( ( context ) => {
|
dispatchInstallPluginMock.mockImplementationOnce( ( context ) => {
|
||||||
|
@ -88,3 +89,10 @@ describe( 'pluginInstallerMachine', () => {
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// TODO: write more tests, I ran out of time and it's friday night
|
||||||
|
// we need tests for:
|
||||||
|
// 1. when given multiple plugins it should call the installPlugin service multiple times with the right plugins
|
||||||
|
// 2. when given multiple plugins and a mocked delay using the config, we can mock the installs to take longer than the timeout and then some plugins should not finish installing, then it should add the remaining to async queue
|
||||||
|
// 3. when a plugin gives an error it should report the error to the parents. we can check this by mocking 'updateParentWithInstallationErrors'
|
||||||
|
// 4. it should update parent with the plugin installation progress, we can check this by mocking the action 'updateParentWithPluginProgress'
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-select-control__option {
|
.woocommerce-select-control__option {
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-profiler-intro-opt-in__content {
|
.woocommerce-profiler-intro-opt-in__content {
|
||||||
padding-top: 81px;
|
padding-top: 110px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-profiler-welcome-image {
|
.woocommerce-profiler-welcome-image {
|
||||||
margin-bottom: 48px;
|
margin-bottom: 64px;
|
||||||
width: 266px;
|
width: 266px;
|
||||||
height: 172px;
|
height: 172px;
|
||||||
background: url(./assets/images/welcome-desktop.svg) no-repeat center
|
background: url(./assets/images/welcome-desktop.svg) no-repeat center
|
||||||
|
@ -145,10 +145,12 @@
|
||||||
.woocommerce-profiler-setup-store__button {
|
.woocommerce-profiler-setup-store__button {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 54px;
|
height: 48px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -171,6 +173,7 @@
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.components-checkbox-control__input-container {
|
.components-checkbox-control__input-container {
|
||||||
|
@ -248,43 +251,47 @@
|
||||||
top: 40px !important;
|
top: 40px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-profiler-business-location {
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.woocommerce-profiler-business-location {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.woocommerce-profiler-business-location__content {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-self: center;
|
||||||
.woocommerce-profiler-business-location__content {
|
& > div {
|
||||||
max-width: 550px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
}
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
& > div {
|
.woocommerce-profiler-heading {
|
||||||
width: 100%;
|
max-width: 570px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.components-base-control__field {
|
.components-base-control__field {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-select-control__control-icon {
|
.woocommerce-select-control__control-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-select-control__control.is-active {
|
||||||
|
.components-base-control__label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-select-control__control.is-active {
|
.woocommerce-select-control.is-searchable
|
||||||
.components-base-control__label {
|
.woocommerce-select-control__control-input {
|
||||||
display: none;
|
margin: 0;
|
||||||
}
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-select-control.is-searchable
|
.woocommerce-select-control.is-searchable
|
||||||
.woocommerce-select-control__control-input {
|
.components-base-control__label {
|
||||||
margin: 0;
|
left: 13px;
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.woocommerce-select-control.is-searchable
|
|
||||||
.components-base-control__label {
|
|
||||||
left: 13px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -398,8 +405,12 @@
|
||||||
max-width: 615px;
|
max-width: 615px;
|
||||||
margin: 58px 0 0 0;
|
margin: 58px 0 0 0;
|
||||||
}
|
}
|
||||||
|
.woocommerce-profiler-heading__title {
|
||||||
|
color: $gray-900;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
.woocommerce-profiler-heading__subtitle {
|
.woocommerce-profiler-heading__subtitle {
|
||||||
margin: 0 0 48px 0 !important;
|
margin: 12px 0 48px 0 !important;
|
||||||
@include breakpoint( '<782px' ) {
|
@include breakpoint( '<782px' ) {
|
||||||
margin-top: 12px !important;
|
margin-top: 12px !important;
|
||||||
}
|
}
|
||||||
|
@ -418,6 +429,8 @@
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: block;
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-error {
|
.plugin-error {
|
||||||
|
@ -506,6 +519,7 @@
|
||||||
border-color: #bbb;
|
border-color: #bbb;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-profiler-select-control__industry {
|
.woocommerce-profiler-select-control__industry {
|
||||||
|
|
|
@ -3086,7 +3086,7 @@ Object {
|
||||||
class="components-button woocommerce-profiler-navigation-skip-link is-link"
|
class="components-button woocommerce-profiler-navigation-skip-link is-link"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Skip this setup
|
Skip this step
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3267,7 +3267,7 @@ Object {
|
||||||
class="components-button woocommerce-profiler-navigation-skip-link is-link"
|
class="components-button woocommerce-profiler-navigation-skip-link is-link"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Skip this setup
|
Skip this step
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,23 +2,25 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Guide } from '@wordpress/components';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import Guide from '../components/guide';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCloseGuide: () => void;
|
onCloseGuide: ( currentPage: number, origin: 'close' | 'finish' ) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BlockEditorGuide = ( { onCloseGuide }: Props ) => {
|
const BlockEditorGuide = ( { onCloseGuide }: Props ) => {
|
||||||
return (
|
return (
|
||||||
<Guide
|
<Guide
|
||||||
className="woocommerce-block-editor-guide"
|
className="woocommerce-block-editor-guide"
|
||||||
finishButtonText={ __( 'Close', 'woocommerce' ) }
|
contentLabel=""
|
||||||
|
finishButtonText={ __( 'Tell me more', 'woocommerce' ) }
|
||||||
|
finishButtonLink="https://woocommerce.com/product-form-beta"
|
||||||
onFinish={ onCloseGuide }
|
onFinish={ onCloseGuide }
|
||||||
pages={ [
|
pages={ [
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Pill, TourKit } from '@woocommerce/components';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { useEffect, useState } from '@wordpress/element';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
|
import { __experimentalUseFeedbackBar as useFeedbackBar } from '@woocommerce/product-editor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -27,17 +28,35 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
|
||||||
|
|
||||||
const [ isGuideOpen, setIsGuideOpen ] = useState( false );
|
const [ isGuideOpen, setIsGuideOpen ] = useState( false );
|
||||||
|
|
||||||
|
const { maybeShowFeedbackBar } = useFeedbackBar();
|
||||||
|
|
||||||
const openGuide = () => {
|
const openGuide = () => {
|
||||||
setIsGuideOpen( true );
|
setIsGuideOpen( true );
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeGuide = () => {
|
|
||||||
recordEvent( 'block_product_editor_spotlight_completed' );
|
|
||||||
setIsGuideOpen( false );
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( isGuideOpen ) {
|
if ( isGuideOpen ) {
|
||||||
return <BlockEditorGuide onCloseGuide={ closeGuide } />;
|
return (
|
||||||
|
<BlockEditorGuide
|
||||||
|
onCloseGuide={ ( currentPage, source ) => {
|
||||||
|
dismissModal();
|
||||||
|
if ( source === 'finish' ) {
|
||||||
|
recordEvent(
|
||||||
|
'block_product_editor_spotlight_tell_me_more_click'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// adding 1 to consider the TourKit as page 0
|
||||||
|
recordEvent(
|
||||||
|
'block_product_editor_spotlight_dismissed',
|
||||||
|
{
|
||||||
|
current_page: currentPage + 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setIsGuideOpen( false );
|
||||||
|
maybeShowFeedbackBar();
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if ( shouldTourBeShown ) {
|
} else if ( shouldTourBeShown ) {
|
||||||
return (
|
return (
|
||||||
<TourKit
|
<TourKit
|
||||||
|
@ -78,16 +97,20 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
closeHandler: ( _steps, _currentStepIndex, source ) => {
|
closeHandler: ( _steps, _currentStepIndex, source ) => {
|
||||||
dismissModal();
|
|
||||||
if ( source === 'done-btn' ) {
|
if ( source === 'done-btn' ) {
|
||||||
recordEvent(
|
recordEvent(
|
||||||
'block_product_editor_spotlight_view_highlights'
|
'block_product_editor_spotlight_view_highlights'
|
||||||
);
|
);
|
||||||
openGuide();
|
openGuide();
|
||||||
} else {
|
} else {
|
||||||
|
dismissModal();
|
||||||
recordEvent(
|
recordEvent(
|
||||||
'block_product_editor_spotlight_dismissed'
|
'block_product_editor_spotlight_dismissed',
|
||||||
|
{
|
||||||
|
current_page: 0,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
maybeShowFeedbackBar();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
@ -116,6 +139,7 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
classNames: 'woocommerce-block-editor-tourkit',
|
||||||
},
|
},
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
$background-height: 220px;
|
$background-height: 220px;
|
||||||
$yellow: #f5e6ab;
|
$yellow: #f5e6ab;
|
||||||
|
$light-purple: #f2edff;
|
||||||
|
|
||||||
.woocommerce-block-editor-guide {
|
.woocommerce-block-editor-guide {
|
||||||
&__background1 {
|
&__background1 {
|
||||||
height: $background-height;
|
height: $background-height;
|
||||||
background-color: #f2edff;
|
background-color: $light-purple;
|
||||||
}
|
}
|
||||||
&__background2 {
|
&__background2 {
|
||||||
height: $background-height;
|
height: $background-height;
|
||||||
|
@ -56,3 +57,12 @@ $yellow: #f5e6ab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-block-editor-tourkit {
|
||||||
|
.components-card__header {
|
||||||
|
align-items: flex-start;
|
||||||
|
height: 200px;
|
||||||
|
background-color: $light-purple;
|
||||||
|
margin-bottom: $gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { SVG, Circle } from '@wordpress/primitives';
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
|
||||||
|
export const PageControlIcon = ( { isSelected }: { isSelected: boolean } ) => (
|
||||||
|
<SVG width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<Circle
|
||||||
|
cx="4"
|
||||||
|
cy="4"
|
||||||
|
r="4"
|
||||||
|
fill={ isSelected ? '#419ECD' : '#E1E3E6' }
|
||||||
|
/>
|
||||||
|
</SVG>
|
||||||
|
);
|
|
@ -0,0 +1,123 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { useState, useRef, createElement } from '@wordpress/element';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Modal, Button } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import PageControl from './page-control';
|
||||||
|
import type { GuideProps } from './types';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This component was copied from @wordpress/components since we needed
|
||||||
|
* additional functionality and also found some issues.
|
||||||
|
* 1: The Close button was being focused every time the page changed.
|
||||||
|
* 2: It was not possible to know if the Guide was closed because the modal was closed or because the Finish button was clicked.
|
||||||
|
* 3: It was not possible to know which was the current page when the modal was closed.
|
||||||
|
* 4: It was not possible to provide a link to the Finish button.
|
||||||
|
*
|
||||||
|
* If/when all those are implemented at some point, we can migrate to the original component.
|
||||||
|
*/
|
||||||
|
function Guide( {
|
||||||
|
className,
|
||||||
|
contentLabel,
|
||||||
|
finishButtonText = __( 'Finish', 'woocommerce' ),
|
||||||
|
finishButtonLink,
|
||||||
|
onFinish,
|
||||||
|
pages = [],
|
||||||
|
}: GuideProps ) {
|
||||||
|
const guideContainer = useRef< HTMLDivElement >( null );
|
||||||
|
const [ currentPage, setCurrentPage ] = useState( 0 );
|
||||||
|
const canGoBack = currentPage > 0;
|
||||||
|
const canGoForward = currentPage < pages.length - 1;
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
if ( canGoBack ) {
|
||||||
|
setCurrentPage( currentPage - 1 );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goForward = () => {
|
||||||
|
if ( canGoForward ) {
|
||||||
|
setCurrentPage( currentPage + 1 );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( pages.length === 0 ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={ classnames( 'components-guide', className ) }
|
||||||
|
title={ contentLabel }
|
||||||
|
onRequestClose={ () => {
|
||||||
|
onFinish( currentPage, 'close' );
|
||||||
|
} }
|
||||||
|
onKeyDown={ ( event ) => {
|
||||||
|
if ( event.code === 'ArrowLeft' ) {
|
||||||
|
goBack();
|
||||||
|
// Do not scroll the modal's contents.
|
||||||
|
event.preventDefault();
|
||||||
|
} else if ( event.code === 'ArrowRight' ) {
|
||||||
|
goForward();
|
||||||
|
// Do not scroll the modal's contents.
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
} }
|
||||||
|
ref={ guideContainer }
|
||||||
|
>
|
||||||
|
<div className="components-guide__container">
|
||||||
|
<div className="components-guide__page">
|
||||||
|
{ pages[ currentPage ].image }
|
||||||
|
|
||||||
|
{ pages.length > 1 && (
|
||||||
|
<PageControl
|
||||||
|
currentPage={ currentPage }
|
||||||
|
numberOfPages={ pages.length }
|
||||||
|
setCurrentPage={ setCurrentPage }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
|
||||||
|
{ pages[ currentPage ].content }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="components-guide__footer">
|
||||||
|
{ canGoBack && (
|
||||||
|
<Button
|
||||||
|
className="components-guide__back-button"
|
||||||
|
onClick={ goBack }
|
||||||
|
>
|
||||||
|
{ __( 'Previous', 'woocommerce' ) }
|
||||||
|
</Button>
|
||||||
|
) }
|
||||||
|
{ canGoForward && (
|
||||||
|
<Button
|
||||||
|
className="components-guide__forward-button"
|
||||||
|
onClick={ goForward }
|
||||||
|
>
|
||||||
|
{ __( 'Next', 'woocommerce' ) }
|
||||||
|
</Button>
|
||||||
|
) }
|
||||||
|
{ ! canGoForward && (
|
||||||
|
<Button
|
||||||
|
className="components-guide__finish-button"
|
||||||
|
href={ finishButtonLink }
|
||||||
|
target={ finishButtonLink ? '_blank' : undefined }
|
||||||
|
rel={ finishButtonLink ? 'noopener' : undefined }
|
||||||
|
onClick={ () => onFinish( currentPage, 'finish' ) }
|
||||||
|
>
|
||||||
|
{ finishButtonText }
|
||||||
|
</Button>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Guide;
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { PageControlIcon } from './icons';
|
||||||
|
import type { PageControlProps } from './types';
|
||||||
|
|
||||||
|
export default function PageControl( {
|
||||||
|
currentPage,
|
||||||
|
numberOfPages,
|
||||||
|
setCurrentPage,
|
||||||
|
}: PageControlProps ) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className="components-guide__page-control"
|
||||||
|
aria-label={ __( 'Guide controls', 'woocommerce' ) }
|
||||||
|
>
|
||||||
|
{ Array.from( { length: numberOfPages } ).map( ( _, page ) => (
|
||||||
|
<li
|
||||||
|
key={ page }
|
||||||
|
// Set aria-current="step" on the active page, see https://www.w3.org/TR/wai-aria-1.1/#aria-current
|
||||||
|
aria-current={ page === currentPage ? 'step' : undefined }
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
key={ page }
|
||||||
|
icon={
|
||||||
|
<PageControlIcon
|
||||||
|
isSelected={ page === currentPage }
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
aria-label={ sprintf(
|
||||||
|
/* translators: 1: current page number 2: total number of pages */
|
||||||
|
__( 'Page %1$d of %2$d', 'woocommerce' ),
|
||||||
|
page + 1,
|
||||||
|
numberOfPages
|
||||||
|
) }
|
||||||
|
onClick={ () => setCurrentPage( page ) }
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
) ) }
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
.components-guide {
|
||||||
|
$image-height: 300px;
|
||||||
|
$image-width: 320px;
|
||||||
|
|
||||||
|
@include break-small() {
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-modal__content {
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
border-radius: $radius-block-ui;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-modal__header {
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0;
|
||||||
|
position: sticky;
|
||||||
|
height: $header-height;
|
||||||
|
|
||||||
|
.components-button {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin: $grid-unit-10 $grid-unit-10 0 0;
|
||||||
|
position: static;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
svg {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: -$header-height;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include break-small() {
|
||||||
|
min-height: $image-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
align-content: center;
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 0 $grid-unit-30 0;
|
||||||
|
padding: 0 $grid-unit-40;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__page-control {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-button {
|
||||||
|
height: 30px;
|
||||||
|
min-width: 20px;
|
||||||
|
margin: -6px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-modal__frame.components-guide {
|
||||||
|
border: none;
|
||||||
|
min-width: 312px;
|
||||||
|
height: 80vh;
|
||||||
|
max-height: 575px;
|
||||||
|
|
||||||
|
@media ( max-width: $break-small ) {
|
||||||
|
margin: auto;
|
||||||
|
max-width: calc(100vw - #{$grid-unit-20} * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-button {
|
||||||
|
&.components-guide__back-button,
|
||||||
|
&.components-guide__forward-button,
|
||||||
|
&.components-guide__finish-button {
|
||||||
|
height: 30px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.components-guide__back-button,
|
||||||
|
&.components-guide__forward-button {
|
||||||
|
font-size: $default-font-size;
|
||||||
|
padding: 4px 2px;
|
||||||
|
|
||||||
|
&.has-text svg {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.components-guide__back-button {
|
||||||
|
left: $grid-unit-40;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.components-guide__forward-button {
|
||||||
|
right: $grid-unit-40;
|
||||||
|
color: #1386bf;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.components-guide__finish-button {
|
||||||
|
right: $grid-unit-40;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export type Page = {
|
||||||
|
/**
|
||||||
|
* Content of the page.
|
||||||
|
*/
|
||||||
|
content: ReactNode;
|
||||||
|
/**
|
||||||
|
* Image displayed above the page content.
|
||||||
|
*/
|
||||||
|
image?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GuideProps = {
|
||||||
|
/**
|
||||||
|
* Deprecated. Use `pages` prop instead.
|
||||||
|
*
|
||||||
|
* @deprecated since 5.5
|
||||||
|
*/
|
||||||
|
children?: ReactNode;
|
||||||
|
/**
|
||||||
|
* A custom class to add to the modal.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Used as the modal's accessibility label.
|
||||||
|
*/
|
||||||
|
contentLabel: string;
|
||||||
|
/**
|
||||||
|
* Use this to customize the label of the _Finish_ button shown at the end of the guide.
|
||||||
|
*
|
||||||
|
* @default 'Finish'
|
||||||
|
*/
|
||||||
|
finishButtonText?: string;
|
||||||
|
/**
|
||||||
|
* Use this to customize href of the _Finish_ button shown at the end of the guide.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
finishButtonLink?: string;
|
||||||
|
/**
|
||||||
|
* A function which is called when the guide is closed, either through closing the dialog or clicking the Finish button.
|
||||||
|
*/
|
||||||
|
onFinish: ( currentPage: number, origin: 'close' | 'finish' ) => void;
|
||||||
|
/**
|
||||||
|
* A list of objects describing each page in the guide. Each object **must** contain a `'content'` property and may optionally contain a `'image'` property.
|
||||||
|
*
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
pages?: Page[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageControlProps = {
|
||||||
|
/**
|
||||||
|
* Current page index.
|
||||||
|
*/
|
||||||
|
currentPage: number;
|
||||||
|
/**
|
||||||
|
* Total number of pages.
|
||||||
|
*/
|
||||||
|
numberOfPages: number;
|
||||||
|
/**
|
||||||
|
* Called when user clicks on a `PageControlIcon` button.
|
||||||
|
*/
|
||||||
|
setCurrentPage: ( page: number ) => void;
|
||||||
|
};
|
|
@ -541,6 +541,27 @@ const attachProductAttributesTracks = () => {
|
||||||
} );
|
} );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const attachGeneralTabTracks = () => {
|
||||||
|
document
|
||||||
|
.querySelector(
|
||||||
|
'#general_product_data .woocommerce-message .variations-tab-navigation-link'
|
||||||
|
)
|
||||||
|
?.addEventListener( 'click', () => {
|
||||||
|
recordEvent( 'disabled_general_tab', {
|
||||||
|
action: 'go_to_variations',
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
document
|
||||||
|
.querySelector(
|
||||||
|
'#general_product_data .woocommerce-message .linked-products-navigation-link'
|
||||||
|
)
|
||||||
|
?.addEventListener( 'click', () => {
|
||||||
|
recordEvent( 'disabled_general_tab', {
|
||||||
|
action: 'go_to_linked_products',
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches product variations tracks.
|
* Attaches product variations tracks.
|
||||||
*/
|
*/
|
||||||
|
@ -731,6 +752,7 @@ export const initProductScreenTracks = () => {
|
||||||
attachProductVariationsTracks();
|
attachProductVariationsTracks();
|
||||||
attachProductTabsTracks();
|
attachProductTabsTracks();
|
||||||
attachProductInventoryTabTracks();
|
attachProductInventoryTabTracks();
|
||||||
|
attachGeneralTabTracks();
|
||||||
};
|
};
|
||||||
|
|
||||||
export function addExitPageListener( pageId: string ) {
|
export function addExitPageListener( pageId: string ) {
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1698_35803)">
|
||||||
|
<g clip-path="url(#clip1_1698_35803)">
|
||||||
|
<path d="M25.9029 14.3535C25.9029 13.341 25.8207 12.6022 25.6429 11.8359H14.0809V16.4059H20.8675C20.7308 17.5416 19.9919 19.252 18.3499 20.4013L18.3269 20.5543L21.9826 23.3863L22.2359 23.4115C24.5619 21.2633 25.9029 18.1026 25.9029 14.3535Z" fill="#4285F4"/>
|
||||||
|
<path d="M14.0809 26.3942C17.4058 26.3942 20.197 25.2995 22.2358 23.4113L18.3499 20.401C17.31 21.1262 15.9144 21.6325 14.0809 21.6325C10.8244 21.6325 8.06055 19.4843 7.0753 16.5151L6.93088 16.5274L3.12967 19.4692L3.07996 19.6074C5.10498 23.6302 9.26455 26.3942 14.0809 26.3942Z" fill="#34A853"/>
|
||||||
|
<path d="M7.07547 16.515C6.8155 15.7488 6.66505 14.9278 6.66505 14.0795C6.66505 13.2311 6.8155 12.4102 7.06179 11.6439L7.0549 11.4808L3.20604 8.4917L3.08012 8.5516C2.2455 10.2209 1.7666 12.0955 1.7666 14.0795C1.7666 16.0635 2.2455 17.938 3.08012 19.6073L7.07547 16.515Z" fill="#FBBC05"/>
|
||||||
|
<path d="M14.081 6.52671C16.3933 6.52671 17.9531 7.52554 18.8425 8.36025L22.318 4.9669C20.1835 2.98291 17.4058 1.76514 14.081 1.76514C9.26457 1.76514 5.10499 4.52903 3.07996 8.55173L7.06164 11.6441C8.06057 8.67492 10.8245 6.52671 14.081 6.52671Z" fill="#EB4335"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1698_35803">
|
||||||
|
<rect width="24.7139" height="24.7139" fill="white" transform="translate(1.76526 1.76514)"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_1698_35803">
|
||||||
|
<rect width="24.1504" height="24.7139" fill="white" transform="translate(1.76526 1.76514)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1698_35800)">
|
||||||
|
<path d="M13.9767 1.75C7.23333 1.75 1.75 7.23333 1.75 13.9767C1.75 20.72 7.23333 26.2033 13.9767 26.2033C20.72 26.2033 26.2033 20.72 26.2033 13.9767C26.2033 7.23333 20.72 1.75 13.9767 1.75ZM13.3467 16.0067H7.25667L13.3467 4.15333V16.0067ZM14.5833 23.7767V11.9233H20.6733L14.5833 23.7767Z" fill="#069E08"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1698_35800">
|
||||||
|
<rect width="24.5" height="24.5" fill="white" transform="translate(1.75 1.75)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 594 B |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,15 @@
|
||||||
|
<svg width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1698_35810)">
|
||||||
|
<g clip-path="url(#clip1_1698_35810)">
|
||||||
|
<path d="M1.99438 14.0002C1.99438 19.0164 5.01076 23.3257 9.32706 25.2203C9.29261 24.3649 9.32094 23.338 9.54029 22.4073C9.77582 21.4129 11.1165 15.7322 11.1165 15.7322C11.1165 15.7322 10.7252 14.95 10.7252 13.794C10.7252 11.9787 11.7774 10.6228 13.0877 10.6228C14.202 10.6228 14.7403 11.4598 14.7403 12.462C14.7403 13.5821 14.0259 15.2575 13.6585 16.8094C13.3516 18.1088 14.31 19.1687 15.592 19.1687C17.913 19.1687 19.4762 16.1877 19.4762 12.6557C19.4762 9.9708 17.6679 7.96123 14.3788 7.96123C10.6629 7.96123 8.34792 10.7324 8.34792 13.8278C8.34792 14.8951 8.6626 15.6477 9.15547 16.2306C9.38209 16.4982 9.41358 16.6059 9.33156 16.9133C9.2728 17.1387 9.13786 17.6813 9.08197 17.8964C9.00043 18.2066 8.74902 18.3176 8.46861 18.203C6.75705 17.5043 5.95994 15.6299 5.95994 13.5229C5.95994 10.0431 8.89477 5.8704 14.7151 5.8704C19.3921 5.8704 22.4704 9.25485 22.4704 12.8878C22.4704 17.6934 19.7987 21.2835 15.8605 21.2835C14.538 21.2835 13.294 20.5686 12.8678 19.7565C12.8678 19.7565 12.1566 22.579 12.006 23.1241C11.7462 24.0685 11.2379 25.0126 10.773 25.7483C11.8748 26.0735 13.0386 26.2507 14.2449 26.2507C21.0095 26.2507 26.4945 20.766 26.4945 14.0002C26.4945 7.23465 21.0095 1.75 14.2449 1.75C7.47951 1.75 1.99438 7.23465 1.99438 14.0002Z" fill="#CB1F27"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1698_35810">
|
||||||
|
<rect width="24.5" height="24.5" fill="white" transform="translate(1.99438 1.75)"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip1_1698_35810">
|
||||||
|
<rect width="24.5" height="24.5" fill="white" transform="translate(1.99438 1.75)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 278 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_1698_35791)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.5751 7H2.61755C1.16468 7 -0.0114466 8.18766 8.40658e-05 9.62899V18.3923C8.40658e-05 19.8452 1.17621 21.0213 2.62908 21.0213H13.5025L18.4722 23.7887L17.3422 21.0213H25.5751C27.028 21.0213 28.2041 19.8452 28.2041 18.3923V9.62899C28.2041 8.17613 27.028 7 25.5751 7ZM2.13267 9.03307C1.80982 9.05613 1.56767 9.17144 1.40624 9.39052C1.24481 9.59807 1.18716 9.86328 1.22175 10.1631C1.90206 14.4871 2.53625 17.4043 3.12431 18.9149C3.35493 19.4683 3.62013 19.7335 3.93146 19.7105C4.41575 19.6759 4.99228 19.0071 5.67259 17.7041C6.03005 16.9662 6.58352 15.8592 7.33301 14.3833C7.95567 16.5626 8.80894 18.2 9.88129 19.2954C10.1811 19.6067 10.4924 19.7451 10.7922 19.722C11.0574 19.699 11.265 19.5606 11.4033 19.3069C11.5186 19.0878 11.5648 18.8341 11.5417 18.5459C11.4725 17.4966 11.5763 16.0322 11.8646 14.1527C12.1644 12.2155 12.5334 10.8203 12.983 9.99012C13.0753 9.81716 13.1099 9.64419 13.0984 9.43664C13.0753 9.17144 12.96 8.95236 12.7409 8.7794C12.5218 8.60643 12.2797 8.52572 12.0145 8.54878C11.6801 8.57184 11.4264 8.73327 11.2534 9.05613C10.5385 10.3591 10.0312 12.4692 9.73139 15.398C9.29323 14.2911 8.92425 12.9881 8.63598 11.4545C8.50914 10.7742 8.19781 10.4513 7.69046 10.4859C7.34454 10.509 7.05628 10.7396 6.82566 11.1778L4.30044 15.9861C3.88534 14.3141 3.49329 12.2732 3.13584 9.86328C3.05513 9.26368 2.72074 8.98695 2.13267 9.03307ZM24.3521 9.86167C25.1707 10.0346 25.7819 10.4728 26.197 11.1992C26.566 11.8219 26.7504 12.5714 26.7504 13.4708C26.7504 14.6584 26.4506 15.7423 25.8511 16.734C25.1592 17.887 24.2598 18.4636 23.1413 18.4636C22.9453 18.4636 22.7378 18.4405 22.5187 18.3944C21.7 18.2214 21.0889 17.7832 20.6738 17.0568C20.3048 16.4226 20.1203 15.6616 20.1203 14.7737C20.1203 13.5861 20.4201 12.5022 21.0197 11.5221C21.7231 10.369 22.6225 9.79248 23.7294 9.79248C23.9254 9.79248 24.133 9.81554 24.3521 9.86167ZM23.8678 16.0998C24.2944 15.7193 24.5827 15.1542 24.7441 14.3932C24.7902 14.128 24.8248 13.8397 24.8248 13.54C24.8248 13.2056 24.7556 12.8481 24.6173 12.4907C24.4443 12.041 24.2137 11.7988 23.937 11.7412C23.5219 11.6604 23.1183 11.8911 22.7378 12.4561C22.4264 12.8942 22.2304 13.3555 22.1266 13.8282C22.069 14.0934 22.0459 14.3817 22.0459 14.67C22.0459 15.0044 22.1151 15.3618 22.2535 15.7192C22.4264 16.1689 22.657 16.4111 22.9338 16.4687C23.2221 16.5264 23.5334 16.3996 23.8678 16.0998ZM18.9674 11.1992C18.5523 10.4728 17.9296 10.0346 17.1225 9.86167C16.9034 9.81554 16.6959 9.79248 16.4998 9.79248C15.3929 9.79248 14.4935 10.369 13.7901 11.5221C13.1905 12.5022 12.8907 13.5861 12.8907 14.7737C12.8907 15.6616 13.0752 16.4226 13.4442 17.0568C13.8593 17.7832 14.4704 18.2214 15.2891 18.3944C15.5082 18.4405 15.7157 18.4636 15.9118 18.4636C17.0302 18.4636 17.9296 17.887 18.6215 16.734C19.2211 15.7423 19.5209 14.6584 19.5209 13.4708C19.5209 12.5714 19.3364 11.8219 18.9674 11.1992ZM17.5145 14.3932C17.3531 15.1542 17.0648 15.7193 16.6382 16.0998C16.3038 16.3996 15.9925 16.5264 15.7042 16.4687C15.4275 16.4111 15.1969 16.1689 15.0239 15.7192C14.8855 15.3618 14.8164 15.0044 14.8164 14.67C14.8164 14.3817 14.8394 14.0934 14.8971 13.8282C15.0008 13.3555 15.1969 12.8942 15.5082 12.4561C15.8887 11.8911 16.2923 11.6604 16.7074 11.7412C16.9841 11.7988 17.2147 12.041 17.3877 12.4907C17.5261 12.8481 17.5952 13.2056 17.5952 13.54C17.5952 13.8397 17.5722 14.128 17.5145 14.3932Z" fill="#7F54B3"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_1698_35791">
|
||||||
|
<rect width="28" height="28" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Always show pricing group fields, disable if not available for a product type
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Provide a data-store agnostic way of untrashing orders.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Fix flakiness in `can set variation defaults` test.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Handle possibly empty refund value in reports (PHP 8.1+).
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Add support for taxonomy meta boxes in HPOS order edit screen.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
improve get_children transient validation
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add re-migrate support to HPOS CLI.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Ensure order ordering in order filter unit test
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Address possible PHP warning in wc-admin/options REST endpoint.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Additional changes for the core profiler plugins page
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Visual changes for the core profiler pages -- intro, guided setup, and skipped guided setup pages
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: enhancement
|
||||||
|
Comment: Add wcadmin_settings_change tracks event when adding/removing entries in shipping
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Show feedback bar after product block editor tour/guide
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Add 'Tell me more' button to end of block editor tour for more information
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: tweak
|
||||||
|
Comment: Update styles of block editor tour
|
||||||
|
|
||||||
|
|
|
@ -959,6 +959,7 @@
|
||||||
|
|
||||||
#variable_product_options #message,
|
#variable_product_options #message,
|
||||||
#inventory_product_data .notice,
|
#inventory_product_data .notice,
|
||||||
|
#general_product_data .notice,
|
||||||
#variable_product_options .notice {
|
#variable_product_options .notice {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
@ -5363,6 +5364,7 @@ img.help_tip {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 0 0 -150px;
|
margin: 0 0 0 -150px;
|
||||||
|
|
||||||
|
|
||||||
.req {
|
.req {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
@ -5379,6 +5381,14 @@ img.help_tip {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
|
input {
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
input:not([type="checkbox"]):not([type="radio"]) + .description {
|
input:not([type="checkbox"]):not([type="radio"]) + .description {
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
|
|
@ -139,6 +139,7 @@ jQuery( function ( $ ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
show_and_hide_panels();
|
show_and_hide_panels();
|
||||||
|
disable_or_enable_fields();
|
||||||
change_product_type_tip( get_product_tip_content( select_val ) );
|
change_product_type_tip( get_product_tip_content( select_val ) );
|
||||||
|
|
||||||
$( 'ul.wc-tabs li:visible' ).eq( 0 ).find( 'a' ).trigger( 'click' );
|
$( 'ul.wc-tabs li:visible' ).eq( 0 ).find( 'a' ).trigger( 'click' );
|
||||||
|
@ -261,6 +262,39 @@ jQuery( function ( $ ) {
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disable_or_enable_fields() {
|
||||||
|
var product_type = $( 'select#product-type' ).val();
|
||||||
|
var hasDisabledFields = true;
|
||||||
|
$( `.enable_if_simple` ).each( function () {
|
||||||
|
$( this ).addClass( 'disabled' );
|
||||||
|
if ( $( this ).is( 'input' ) ) {
|
||||||
|
$( this ).prop( 'disabled', true );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
$( `.enable_if_external` ).each( function () {
|
||||||
|
$( this ).addClass( 'disabled' );
|
||||||
|
if ( $( this ).is( 'input' ) ) {
|
||||||
|
$( this ).prop( 'disabled', true );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
$( `.enable_if_${ product_type }` ).each( function () {
|
||||||
|
hasDisabledFields = false;
|
||||||
|
$( this ).removeClass( 'disabled' );
|
||||||
|
if ( $( this ).is( 'input' ) ) {
|
||||||
|
$( this ).prop( 'disabled', false );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasDisabledFields &&
|
||||||
|
! $( '#general_product_data .woocommerce-message' ).is( ':visible' )
|
||||||
|
) {
|
||||||
|
$( `.pricing_disabled_fallback_message` ).show();
|
||||||
|
} else {
|
||||||
|
$( `.pricing_disabled_fallback_message` ).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sale price schedule.
|
// Sale price schedule.
|
||||||
$( '.sale_price_dates_fields' ).each( function () {
|
$( '.sale_price_dates_fields' ).each( function () {
|
||||||
var $these_sale_dates = $( this );
|
var $these_sale_dates = $( this );
|
||||||
|
@ -891,7 +925,7 @@ jQuery( function ( $ ) {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Go to attributes tab when clicking on link in variations message
|
// Go to attributes tab when clicking on link in variations message
|
||||||
$( document.body ).on(
|
$( '#woocommerce-product-data' ).on(
|
||||||
'click',
|
'click',
|
||||||
'#variable_product_options .add-attributes-message a[href="#product_attributes"]',
|
'#variable_product_options .add-attributes-message a[href="#product_attributes"]',
|
||||||
function () {
|
function () {
|
||||||
|
@ -902,6 +936,34 @@ jQuery( function ( $ ) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Go to variations tab when clicking on link in the general tab message
|
||||||
|
$( '#woocommerce-product-data' ).on(
|
||||||
|
'click',
|
||||||
|
'#general_product_data .woocommerce-message a[href="#variable_product_options"]',
|
||||||
|
function () {
|
||||||
|
$(
|
||||||
|
'#woocommerce-product-data .variations_tab a[href="#variable_product_options"]'
|
||||||
|
).trigger( 'click' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Go to linked products tab when clicking on link in the general tab message
|
||||||
|
$( '#woocommerce-product-data' ).on(
|
||||||
|
'click',
|
||||||
|
'#general_product_data .woocommerce-message a[href="#linked_product_data"]',
|
||||||
|
function () {
|
||||||
|
$(
|
||||||
|
'#woocommerce-product-data .linked_product_tab a[href="#linked_product_data"]'
|
||||||
|
).trigger( 'click' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Uploading files.
|
// Uploading files.
|
||||||
var downloadable_file_frame;
|
var downloadable_file_frame;
|
||||||
var file_path_field;
|
var file_path_field;
|
||||||
|
|
|
@ -89,7 +89,7 @@ class WC_Meta_Box_Product_Data {
|
||||||
'general' => array(
|
'general' => array(
|
||||||
'label' => __( 'General', 'woocommerce' ),
|
'label' => __( 'General', 'woocommerce' ),
|
||||||
'target' => 'general_product_data',
|
'target' => 'general_product_data',
|
||||||
'class' => array( 'hide_if_grouped' ),
|
'class' => array(),
|
||||||
'priority' => 10,
|
'priority' => 10,
|
||||||
),
|
),
|
||||||
'inventory' => array(
|
'inventory' => array(
|
||||||
|
|
|
@ -36,14 +36,61 @@ defined( 'ABSPATH' ) || exit;
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="options_group pricing show_if_simple show_if_external hidden">
|
<div class="inline notice woocommerce-message show_if_variable">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Allow developers to change the general pricing message.
|
||||||
|
*
|
||||||
|
* @since 7.9.0
|
||||||
|
*/
|
||||||
|
echo esc_html( apply_filters( 'woocommerce_general_pricing_disabled_message', __( 'You can manage pricing and other details individually for each product variation.', 'woocommerce' ), 'variable' ) );
|
||||||
|
?>
|
||||||
|
<a class="variations-tab-navigation-link" href="#variable_product_options">
|
||||||
|
<?php
|
||||||
|
esc_html_e( 'Go to Variations', 'woocommerce' );
|
||||||
|
?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline notice woocommerce-message show_if_grouped">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Allow developers to change the general pricing message.
|
||||||
|
*
|
||||||
|
* @since 7.9.0
|
||||||
|
*/
|
||||||
|
echo esc_html( apply_filters( 'woocommerce_general_pricing_disabled_message', __( 'You can manage pricing and other details individually for each product added to this group.', 'woocommerce' ), 'grouped' ) );
|
||||||
|
?>
|
||||||
|
<a class="linked-products-navigation-link" href="#linked_product_data"><?php esc_html_e( 'Go to Linked Products', 'woocommerce' ); ?></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="inline notice woocommerce-message pricing_disabled_fallback_message">
|
||||||
|
<p>
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Allow developers to change the general pricing message.
|
||||||
|
*
|
||||||
|
* @since 7.9.0
|
||||||
|
*/
|
||||||
|
echo esc_html( apply_filters( 'woocommerce_general_pricing_disabled_message', __( 'You can manage pricing and other details in one of the other tabs.', 'woocommerce' ), 'grouped' ) );
|
||||||
|
?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options_group pricing">
|
||||||
<?php
|
<?php
|
||||||
woocommerce_wp_text_input(
|
woocommerce_wp_text_input(
|
||||||
array(
|
array(
|
||||||
'id' => '_regular_price',
|
'id' => '_regular_price',
|
||||||
'value' => $product_object->get_regular_price( 'edit' ),
|
'value' => $product_object->get_regular_price( 'edit' ),
|
||||||
'label' => __( 'Regular price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
'label' => __( 'Regular price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||||
'data_type' => 'price',
|
'label_class' => 'enable_if_simple enable_if_external',
|
||||||
|
'data_type' => 'price',
|
||||||
|
'class' => 'enable_if_simple enable_if_external',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -53,7 +100,9 @@ defined( 'ABSPATH' ) || exit;
|
||||||
'value' => $product_object->get_sale_price( 'edit' ),
|
'value' => $product_object->get_sale_price( 'edit' ),
|
||||||
'data_type' => 'price',
|
'data_type' => 'price',
|
||||||
'label' => __( 'Sale price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
'label' => __( 'Sale price', 'woocommerce' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||||
'description' => '<a href="#" class="sale_schedule">' . __( 'Schedule', 'woocommerce' ) . '</a>',
|
'label_class' => 'enable_if_simple enable_if_external',
|
||||||
|
'description' => '<a href="#" class="sale_schedule show_if_simple show_if_external">' . __( 'Schedule', 'woocommerce' ) . '</a>',
|
||||||
|
'class' => 'enable_if_simple enable_if_external',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -93,7 +142,7 @@ defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
if ( $downloadable_files ) {
|
if ( $downloadable_files ) {
|
||||||
foreach ( $downloadable_files as $key => $file ) {
|
foreach ( $downloadable_files as $key => $file ) {
|
||||||
$disabled_download = isset( $file['enabled'] ) && false === $file['enabled'];
|
$disabled_download = isset( $file['enabled'] ) && false === $file['enabled'];
|
||||||
$disabled_downloads_count += (int) $disabled_download;
|
$disabled_downloads_count += (int) $disabled_download;
|
||||||
include __DIR__ . '/html-product-download.php';
|
include __DIR__ . '/html-product-download.php';
|
||||||
}
|
}
|
||||||
|
@ -105,8 +154,8 @@ defined( 'ABSPATH' ) || exit;
|
||||||
<th colspan="2">
|
<th colspan="2">
|
||||||
<a href="#" class="button insert" data-row="
|
<a href="#" class="button insert" data-row="
|
||||||
<?php
|
<?php
|
||||||
$key = '';
|
$key = '';
|
||||||
$file = array(
|
$file = array(
|
||||||
'file' => '',
|
'file' => '',
|
||||||
'name' => '',
|
'name' => '',
|
||||||
);
|
);
|
||||||
|
|
|
@ -512,7 +512,7 @@ class WC_Admin_Report {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $data_key ) {
|
if ( $data_key ) {
|
||||||
$prepared_data[ $time ][1] += $d->$data_key;
|
$prepared_data[ $time ][1] += is_numeric( $d->$data_key ) ? $d->$data_key : 0;
|
||||||
} else {
|
} else {
|
||||||
$prepared_data[ $time ][1] ++;
|
$prepared_data[ $time ][1] ++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,7 @@ class WC_Report_Sales_By_Date extends WC_Admin_Report {
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $this->report_data->partial_refunds as $key => $order ) {
|
foreach ( $this->report_data->partial_refunds as $key => $order ) {
|
||||||
$this->report_data->partial_refunds[ $key ]->net_refund = $order->total_refund - ( $order->total_shipping + $order->total_tax + $order->total_shipping_tax );
|
$this->report_data->partial_refunds[ $key ]->net_refund = (float) $order->total_refund - ( (float) $order->total_shipping + (float) $order->total_tax + (float) $order->total_shipping_tax );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -31,6 +31,7 @@ function woocommerce_wp_text_input( $field, WC_Data $data = null ) {
|
||||||
$field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id'];
|
$field['name'] = isset( $field['name'] ) ? $field['name'] : $field['id'];
|
||||||
$field['type'] = isset( $field['type'] ) ? $field['type'] : 'text';
|
$field['type'] = isset( $field['type'] ) ? $field['type'] : 'text';
|
||||||
$field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;
|
$field['desc_tip'] = isset( $field['desc_tip'] ) ? $field['desc_tip'] : false;
|
||||||
|
$field['label_class'] = isset( $field['label_class'] ) ? $field['label_class'] : '';
|
||||||
$data_type = empty( $field['data_type'] ) ? '' : $field['data_type'];
|
$data_type = empty( $field['data_type'] ) ? '' : $field['data_type'];
|
||||||
|
|
||||||
switch ( $data_type ) {
|
switch ( $data_type ) {
|
||||||
|
@ -65,8 +66,10 @@ function woocommerce_wp_text_input( $field, WC_Data $data = null ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$label_class = ! empty( $field['label_class'] ) ? 'class="' . esc_attr( $field['label_class'] ) . '" ' : '';
|
||||||
|
|
||||||
echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '">
|
echo '<p class="form-field ' . esc_attr( $field['id'] ) . '_field ' . esc_attr( $field['wrapper_class'] ) . '">
|
||||||
<label for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>';
|
<label ' . esc_attr( $label_class ) . ' for="' . esc_attr( $field['id'] ) . '">' . wp_kses_post( $field['label'] ) . '</label>';
|
||||||
|
|
||||||
if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
|
if ( ! empty( $field['description'] ) && false !== $field['desc_tip'] ) {
|
||||||
echo wc_help_tip( $field['description'] );
|
echo wc_help_tip( $field['description'] );
|
||||||
|
|
|
@ -2995,6 +2995,18 @@ class WC_AJAX {
|
||||||
// That's fine, it's not in the database anyways. NEXT!
|
// That's fine, it's not in the database anyways. NEXT!
|
||||||
continue;
|
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 );
|
WC_Shipping_Zones::delete_zone( $zone_id );
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -3024,19 +3036,18 @@ class WC_AJAX {
|
||||||
);
|
);
|
||||||
$zone->set_zone_order( $zone_data['zone_order'] );
|
$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();
|
$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(
|
wp_send_json_success(
|
||||||
array(
|
array(
|
||||||
'zones' => WC_Shipping_Zones::get_zones( 'json' ),
|
'zones' => WC_Shipping_Zones::get_zones( 'json' ),
|
||||||
|
@ -3066,15 +3077,31 @@ class WC_AJAX {
|
||||||
|
|
||||||
$zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
|
$zone_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
|
||||||
$zone = new WC_Shipping_Zone( $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
|
* @since 7.8.0
|
||||||
*/
|
*/
|
||||||
do_action(
|
do_action(
|
||||||
'woocommerce_update_non_option_setting',
|
'woocommerce_update_non_option_setting',
|
||||||
array(
|
array(
|
||||||
'id' => 'zone_method',
|
'id' => 'zone_method',
|
||||||
|
'action' => 'add',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$instance_id = $zone->add_shipping_method( wc_clean( wp_unslash( $_POST['method_id'] ) ) );
|
$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_id = wc_clean( wp_unslash( $_POST['zone_id'] ) );
|
||||||
$zone = new WC_Shipping_Zone( $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
|
$changes = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||||
|
|
||||||
if ( isset( $changes['zone_name'] ) ) {
|
if ( isset( $changes['zone_name'] ) ) {
|
||||||
/**
|
/**
|
||||||
* Completes the saving process for options.
|
* Notifies that a non-option setting has been updated.
|
||||||
*
|
*
|
||||||
* @since 7.8.0
|
* @since 7.8.0
|
||||||
*/
|
*/
|
||||||
|
@ -3192,7 +3234,7 @@ class WC_AJAX {
|
||||||
|
|
||||||
if ( isset( $changes['zone_locations'] ) ) {
|
if ( isset( $changes['zone_locations'] ) ) {
|
||||||
/**
|
/**
|
||||||
* Completes the saving process for options.
|
* Notifies that a non-option setting has been updated.
|
||||||
*
|
*
|
||||||
* @since 7.8.0
|
* @since 7.8.0
|
||||||
*/
|
*/
|
||||||
|
@ -3218,7 +3260,7 @@ class WC_AJAX {
|
||||||
|
|
||||||
if ( isset( $changes['zone_postcodes'] ) ) {
|
if ( isset( $changes['zone_postcodes'] ) ) {
|
||||||
/**
|
/**
|
||||||
* Completes the saving process for options.
|
* Notifies that a non-option setting has been updated.
|
||||||
*
|
*
|
||||||
* @since 7.8.0
|
* @since 7.8.0
|
||||||
*/
|
*/
|
||||||
|
@ -3231,12 +3273,6 @@ class WC_AJAX {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( isset( $changes['methods'] ) ) {
|
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 ) {
|
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 ) );
|
$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();
|
$option_key = $shipping_method->get_instance_option_key();
|
||||||
if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) {
|
if ( $wpdb->delete( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'instance_id' => $instance_id ) ) ) {
|
||||||
delete_option( $option_key );
|
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 );
|
do_action( 'woocommerce_shipping_zone_method_deleted', $instance_id, $method_id, $zone_id );
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
@ -3260,7 +3308,7 @@ class WC_AJAX {
|
||||||
|
|
||||||
if ( isset( $method_data['method_order'] ) ) {
|
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
|
* @since 7.8.0
|
||||||
*/
|
*/
|
||||||
|
@ -3270,7 +3318,7 @@ class WC_AJAX {
|
||||||
|
|
||||||
if ( isset( $method_data['enabled'] ) ) {
|
if ( isset( $method_data['enabled'] ) ) {
|
||||||
/**
|
/**
|
||||||
* Completes the saving process for options.
|
* Notifies that a non-option setting has been updated.
|
||||||
*
|
*
|
||||||
* @since 7.8.0
|
* @since 7.8.0
|
||||||
*/
|
*/
|
||||||
|
@ -3385,6 +3433,18 @@ class WC_AJAX {
|
||||||
// That's fine, it's not in the database anyways. NEXT!
|
// That's fine, it's not in the database anyways. NEXT!
|
||||||
continue;
|
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' );
|
wp_delete_term( $term_id, 'product_shipping_class' );
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -3426,9 +3486,27 @@ class WC_AJAX {
|
||||||
if ( empty( $update_args['name'] ) ) {
|
if ( empty( $update_args['name'] ) ) {
|
||||||
continue;
|
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 );
|
$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'];
|
$term_id = is_wp_error( $inserted_term ) ? 0 : $inserted_term['term_id'];
|
||||||
} else {
|
} 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 );
|
wp_update_term( $term_id, 'product_shipping_class', $update_args );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2286,4 +2286,13 @@ class WC_Order extends WC_Abstract_Order {
|
||||||
public function is_created_via( $modus ) {
|
public function is_created_via( $modus ) {
|
||||||
return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus );
|
return apply_filters( 'woocommerce_order_is_created_via', $modus === $this->get_created_via(), $this, $modus );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to restore the specified order back to its original status (after having been trashed).
|
||||||
|
*
|
||||||
|
* @return bool If the operation was successful.
|
||||||
|
*/
|
||||||
|
public function untrash(): bool {
|
||||||
|
return (bool) $this->data_store->untrash_order( $this );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1181,4 +1181,20 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
|
||||||
);
|
);
|
||||||
WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
|
WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to restore the specified order back to its original status (after having been trashed).
|
||||||
|
*
|
||||||
|
* @param WC_Order $order The order to be untrashed.
|
||||||
|
*
|
||||||
|
* @return bool If the operation was successful.
|
||||||
|
*/
|
||||||
|
public function untrash_order( WC_Order $order ): bool {
|
||||||
|
if ( ! wp_untrash_post( $order->get_id() ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order->set_status( get_post_field( 'post_status', $order->get_id() ) );
|
||||||
|
return (bool) $order->save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,11 +119,11 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
|
||||||
public function read_children( &$product, $force_read = false ) {
|
public function read_children( &$product, $force_read = false ) {
|
||||||
$children_transient_name = 'wc_product_children_' . $product->get_id();
|
$children_transient_name = 'wc_product_children_' . $product->get_id();
|
||||||
$children = get_transient( $children_transient_name );
|
$children = get_transient( $children_transient_name );
|
||||||
if ( false === $children ) {
|
if ( empty( $children ) || ! is_array( $children ) ) {
|
||||||
$children = array();
|
$children = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
|
if ( ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) {
|
||||||
$all_args = array(
|
$all_args = array(
|
||||||
'post_parent' => $product->get_id(),
|
'post_parent' => $product->get_id(),
|
||||||
'post_type' => 'product_variation',
|
'post_type' => 'product_variation',
|
||||||
|
|
|
@ -43,6 +43,21 @@ class WC_Settings_Tracking {
|
||||||
*/
|
*/
|
||||||
protected $modified_options = array();
|
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.
|
* Toggled options.
|
||||||
*
|
*
|
||||||
|
@ -74,7 +89,11 @@ class WC_Settings_Tracking {
|
||||||
if ( ! in_array( $option['id'], $this->allowed_options, true ) ) {
|
if ( ! in_array( $option['id'], $this->allowed_options, true ) ) {
|
||||||
$this->allowed_options[] = $option['id'];
|
$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'];
|
$this->updated_options[] = $option['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,13 +162,23 @@ class WC_Settings_Tracking {
|
||||||
public function send_settings_change_event() {
|
public function send_settings_change_event() {
|
||||||
global $current_tab, $current_section;
|
global $current_tab, $current_section;
|
||||||
|
|
||||||
if ( empty( $this->updated_options ) ) {
|
if ( empty( $this->updated_options ) && empty( $this->deleted_options ) && empty( $this->added_options ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$properties = array(
|
$properties = array();
|
||||||
'settings' => implode( ',', $this->updated_options ),
|
|
||||||
);
|
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 ) {
|
foreach ( $this->toggled_options as $state => $options ) {
|
||||||
if ( ! empty( $options ) ) {
|
if ( ! empty( $options ) ) {
|
||||||
|
|
|
@ -69,9 +69,9 @@ class Options extends \WC_REST_Data_Controller {
|
||||||
* @return WP_Error|boolean
|
* @return WP_Error|boolean
|
||||||
*/
|
*/
|
||||||
public function get_item_permissions_check( $request ) {
|
public function get_item_permissions_check( $request ) {
|
||||||
$params = explode( ',', $request['options'] );
|
$params = ( isset( $request['options'] ) && is_string( $request['options'] ) ) ? explode( ',', $request['options'] ) : array();
|
||||||
|
|
||||||
if ( ! isset( $request['options'] ) || ! is_array( $params ) ) {
|
if ( ! $params ) {
|
||||||
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'You must supply an array of options.', 'woocommerce' ), 500 );
|
return new \WP_Error( 'woocommerce_rest_cannot_view', __( 'You must supply an array of options.', 'woocommerce' ), 500 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,13 +233,13 @@ class Options extends \WC_REST_Data_Controller {
|
||||||
* @return array Options object with option values.
|
* @return array Options object with option values.
|
||||||
*/
|
*/
|
||||||
public function get_options( $request ) {
|
public function get_options( $request ) {
|
||||||
$params = explode( ',', $request['options'] );
|
|
||||||
$options = array();
|
$options = array();
|
||||||
|
|
||||||
if ( ! is_array( $params ) ) {
|
if ( empty( $request['options'] ) || ! is_string( $request['options'] ) ) {
|
||||||
return array();
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$params = explode( ',', $request['options'] );
|
||||||
foreach ( $params as $option ) {
|
foreach ( $params as $option ) {
|
||||||
$options[ $option ] = get_option( $option );
|
$options[ $option ] = get_option( $option );
|
||||||
}
|
}
|
||||||
|
|
|
@ -304,6 +304,11 @@ class CLIRunner {
|
||||||
* ---
|
* ---
|
||||||
* default: Output of function `wc_get_order_types( 'cot-migration' )`
|
* 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
|
* ## EXAMPLES
|
||||||
*
|
*
|
||||||
* # Verify migrated order data, 500 orders at a time.
|
* # Verify migrated order data, 500 orders at a time.
|
||||||
|
@ -327,6 +332,7 @@ class CLIRunner {
|
||||||
'end-at' => - 1,
|
'end-at' => - 1,
|
||||||
'verbose' => false,
|
'verbose' => false,
|
||||||
'order-types' => '',
|
'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'];
|
$batch_size = ( (int) $assoc_args['batch-size'] ) === 0 ? 500 : (int) $assoc_args['batch-size'];
|
||||||
$verbose = (bool) $assoc_args['verbose'];
|
$verbose = (bool) $assoc_args['verbose'];
|
||||||
$order_types = wc_get_order_types( 'cot-migration' );
|
$order_types = wc_get_order_types( 'cot-migration' );
|
||||||
|
$remigrate = (bool) $assoc_args['re-migrate'];
|
||||||
if ( ! empty( $assoc_args['order-types'] ) ) {
|
if ( ! empty( $assoc_args['order-types'] ) ) {
|
||||||
$passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) );
|
$passed_order_types = array_map( 'trim', explode( ',', $assoc_args['order-types'] ) );
|
||||||
$order_types = array_intersect( $order_types, $passed_order_types );
|
$order_types = array_intersect( $order_types, $passed_order_types );
|
||||||
|
@ -415,6 +422,36 @@ class CLIRunner {
|
||||||
$errors
|
$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();
|
$progress->tick();
|
||||||
|
|
|
@ -243,13 +243,7 @@ abstract class MetaToCustomTableMigrator extends TableMigrator {
|
||||||
$to_insert = array_diff_key( $data['data'], $existing_records );
|
$to_insert = array_diff_key( $data['data'], $existing_records );
|
||||||
$this->process_insert_batch( $to_insert );
|
$this->process_insert_batch( $to_insert );
|
||||||
|
|
||||||
$existing_records = array_filter(
|
$to_update = array_intersect_key( $data['data'], $existing_records );
|
||||||
$existing_records,
|
|
||||||
function( $record_data ) {
|
|
||||||
return '1' === $record_data->modified;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
$to_update = array_intersect_key( $data['data'], $existing_records );
|
|
||||||
$this->process_update_batch( $to_update, $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' ) );
|
$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 );
|
$additional_where = $this->get_additional_where_clause_for_get_data_to_insert_or_update( $entity_ids );
|
||||||
|
|
||||||
$already_migrated_entity_ids = $this->db_get_results(
|
$already_migrated_entity_ids = $this->db_get_results(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- All columns and table names are hardcoded.
|
// 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
|
FROM `$destination_table` destination
|
||||||
JOIN `$source_table` source ON source.`$source_destination_join_column` = destination.`$destination_source_join_column`
|
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
|
WHERE source.`$source_primary_key_column` IN ( $entity_id_placeholder ) $additional_where
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
namespace Automattic\WooCommerce\Internal\Admin\Orders;
|
||||||
|
|
||||||
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
|
||||||
|
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Edit.
|
* Class Edit.
|
||||||
|
@ -26,6 +27,13 @@ class Edit {
|
||||||
*/
|
*/
|
||||||
private $custom_meta_box;
|
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.
|
* Instance of WC_Order to be used in metaboxes.
|
||||||
*
|
*
|
||||||
|
@ -110,10 +118,16 @@ class Edit {
|
||||||
if ( ! isset( $this->custom_meta_box ) ) {
|
if ( ! isset( $this->custom_meta_box ) ) {
|
||||||
$this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class );
|
$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->add_save_meta_boxes();
|
||||||
$this->handle_order_update();
|
$this->handle_order_update();
|
||||||
$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
|
$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
|
||||||
$this->add_order_specific_meta_box();
|
$this->add_order_specific_meta_box();
|
||||||
|
$this->add_order_taxonomies_meta_box();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* From wp-admin/includes/meta-boxes.php.
|
* 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.
|
* 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() );
|
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.
|
* Save meta for shop order.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
|
||||||
|
|
||||||
|
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaxonomiesMetaBox class, renders taxonomy sidebar widget on order edit screen.
|
||||||
|
*/
|
||||||
|
class TaxonomiesMetaBox {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Table data store class.
|
||||||
|
*
|
||||||
|
* @var OrdersTableDataStore
|
||||||
|
*/
|
||||||
|
private $orders_table_data_store;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency injection init method.
|
||||||
|
*
|
||||||
|
* @param OrdersTableDataStore $orders_table_data_store Order Table data store class.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function init( OrdersTableDataStore $orders_table_data_store ) {
|
||||||
|
$this->orders_table_data_store = $orders_table_data_store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers meta boxes to be rendered in order edit screen for taxonomies.
|
||||||
|
*
|
||||||
|
* Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it.
|
||||||
|
*
|
||||||
|
* @param string $screen_id Screen ID.
|
||||||
|
* @param string $order_type Order type to register meta boxes for.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) {
|
||||||
|
include_once ABSPATH . 'wp-admin/includes/meta-boxes.php';
|
||||||
|
$taxonomies = get_object_taxonomies( $order_type );
|
||||||
|
// All taxonomies.
|
||||||
|
foreach ( $taxonomies as $tax_name ) {
|
||||||
|
$taxonomy = get_taxonomy( $tax_name );
|
||||||
|
if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) {
|
||||||
|
$taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) {
|
||||||
|
$taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $taxonomy->labels->name;
|
||||||
|
|
||||||
|
if ( ! is_taxonomy_hierarchical( $tax_name ) ) {
|
||||||
|
$tax_meta_box_id = 'tagsdiv-' . $tax_name;
|
||||||
|
} else {
|
||||||
|
$tax_meta_box_id = $tax_name . 'div';
|
||||||
|
}
|
||||||
|
|
||||||
|
add_meta_box(
|
||||||
|
$tax_meta_box_id,
|
||||||
|
$label,
|
||||||
|
$taxonomy->meta_box_cb,
|
||||||
|
$screen_id,
|
||||||
|
'side',
|
||||||
|
'core',
|
||||||
|
array(
|
||||||
|
'taxonomy' => $tax_name,
|
||||||
|
'__back_compat_meta_box' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save handler for taxonomy data.
|
||||||
|
*
|
||||||
|
* @param \WC_Abstract_Order $order Order object.
|
||||||
|
* @param array|null $taxonomy_input Taxonomy input passed from input.
|
||||||
|
*/
|
||||||
|
public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) {
|
||||||
|
if ( ! isset( $taxonomy_input ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input );
|
||||||
|
|
||||||
|
$sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input );
|
||||||
|
$this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy.
|
||||||
|
*
|
||||||
|
* @param array|null $taxonomy_data Nonce verified taxonomy input.
|
||||||
|
*
|
||||||
|
* @return array Sanitized taxonomy input.
|
||||||
|
*/
|
||||||
|
private function sanitize_tax_input( $taxonomy_data ) : array {
|
||||||
|
$sanitized_tax_input = array();
|
||||||
|
if ( ! is_array( $taxonomy_data ) ) {
|
||||||
|
return $sanitized_tax_input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert taxonomy input to term IDs, to avoid ambiguity.
|
||||||
|
foreach ( $taxonomy_data as $taxonomy => $terms ) {
|
||||||
|
$tax_object = get_taxonomy( $taxonomy );
|
||||||
|
if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) {
|
||||||
|
$sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sanitized_tax_input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box.
|
||||||
|
*
|
||||||
|
* @param \WC_Abstract_Order $order Order object.
|
||||||
|
* @param array $box Meta box args.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function order_categories_meta_box( $order, $box ) {
|
||||||
|
$post = get_post( $order->get_id() );
|
||||||
|
post_categories_meta_box( $post, $box );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box.
|
||||||
|
*
|
||||||
|
* @param \WC_Abstract_Order $order Order object.
|
||||||
|
* @param array $box Meta box args.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function order_tags_meta_box( $order, $box ) {
|
||||||
|
$post = get_post( $order->get_id() );
|
||||||
|
post_tags_meta_box( $post, $box );
|
||||||
|
}
|
||||||
|
}
|
|
@ -816,6 +816,7 @@ class DefaultFreeExtensions {
|
||||||
*
|
*
|
||||||
* - Updated description for the core-profiler.
|
* - Updated description for the core-profiler.
|
||||||
* - Adds learn_more_link and label.
|
* - Adds learn_more_link and label.
|
||||||
|
* - Adds install_priority, which is used to sort the plugins. The value is determined by the plugin size. Lower = smaller.
|
||||||
*
|
*
|
||||||
* @param array $plugins Array of plugins.
|
* @param array $plugins Array of plugins.
|
||||||
*
|
*
|
||||||
|
@ -824,47 +825,72 @@ class DefaultFreeExtensions {
|
||||||
public static function with_core_profiler_fields( array $plugins ) {
|
public static function with_core_profiler_fields( array $plugins ) {
|
||||||
$_plugins = array(
|
$_plugins = array(
|
||||||
'woocommerce-payments' => array(
|
'woocommerce-payments' => array(
|
||||||
'label' => __( 'Get paid with WooCommerce Payments', 'woocommerce' ),
|
'label' => __( 'Get paid with WooCommerce Payments', 'woocommerce' ),
|
||||||
'description' => __( 'Accept credit cards and other popular payment methods smoothly.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments',
|
'description' => __( "Securely accept payments and manage payment activity straight from your store's dashboard", 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/woocommerce-payments',
|
||||||
|
'install_priority' => 5,
|
||||||
),
|
),
|
||||||
'woocommerce-services:shipping' => array(
|
'woocommerce-services:shipping' => array(
|
||||||
'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ),
|
'label' => __( 'Print shipping labels with WooCommerce Shipping', 'woocommerce' ),
|
||||||
'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping',
|
'description' => __( 'Print USPS and DHL labels directly from your dashboard and save on shipping.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/woocommerce-shipping',
|
||||||
|
'install_priority' => 3,
|
||||||
),
|
),
|
||||||
'jetpack' => array(
|
'jetpack' => array(
|
||||||
'label' => __( 'Enhance security with Jetpack', 'woocommerce' ),
|
'label' => __( 'Enhance security with Jetpack', 'woocommerce' ),
|
||||||
'description' => __( 'Get auto real-time backups, malware scans, and spam protection.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-jetpack.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/jetpack',
|
'description' => __( 'Get auto real-time backups, malware scans, and spam protection.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/jetpack',
|
||||||
|
'install_priority' => 8,
|
||||||
),
|
),
|
||||||
'pinterest-for-woocommerce' => array(
|
'pinterest-for-woocommerce' => array(
|
||||||
'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ),
|
'label' => __( 'Showcase your products with Pinterest', 'woocommerce' ),
|
||||||
'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-pinterest.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce',
|
'description' => __( 'Get your products in front of a highly engaged audience.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/pinterest-for-woocommerce',
|
||||||
|
'install_priority' => 2,
|
||||||
),
|
),
|
||||||
'mailpoet' => array(
|
'mailpoet' => array(
|
||||||
'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ),
|
'label' => __( 'Reach your customers with MailPoet', 'woocommerce' ),
|
||||||
'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-mailpoet.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/mailpoet',
|
'description' => __( 'Send purchase follow-up emails, newsletters, and promotional campaigns.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/mailpoet',
|
||||||
|
'install_priority' => 7,
|
||||||
),
|
),
|
||||||
'tiktok-for-business' => array(
|
'tiktok-for-business' => array(
|
||||||
'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ),
|
'label' => __( 'Create ad campaigns with TikTok', 'woocommerce' ),
|
||||||
'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-tiktok.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce',
|
'description' => __( 'Create advertising campaigns and reach one billion global users.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/tiktok-for-woocommerce',
|
||||||
|
'install_priority' => 1,
|
||||||
),
|
),
|
||||||
'google-listings-and-ads' => array(
|
'google-listings-and-ads' => array(
|
||||||
'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ),
|
'label' => __( 'Drive sales with Google Listings & Ads', 'woocommerce' ),
|
||||||
'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-google.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads',
|
'description' => __( 'Reach millions of active shoppers across Google with free product listings and ads.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/google-listings-and-ads',
|
||||||
|
'install_priority' => 6,
|
||||||
),
|
),
|
||||||
'woocommerce-services:tax' => array(
|
'woocommerce-services:tax' => array(
|
||||||
'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ),
|
'label' => __( 'Get automated tax rates with WooCommerce Tax', 'woocommerce' ),
|
||||||
'description' => __( 'Automatically calculate how much sales tax should be collected – by city, country, or state.', 'woocommerce' ),
|
'image_url' => plugins_url( '/assets/images/core-profiler/logo-woo.svg', WC_PLUGIN_FILE ),
|
||||||
'learn_more_link' => 'https://woocommerce.com/products/tax',
|
'description' => __( 'Automatically calculate how much sales tax should be collected – by city, country, or state.', 'woocommerce' ),
|
||||||
|
'learn_more_link' => 'https://woocommerce.com/products/tax',
|
||||||
|
'install_priority' => 4,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Copy shipping for the core-profiler and remove is_visible conditions, except for the country restriction.
|
||||||
|
$_plugins['woocommerce-services:shipping']['is_visible'] = [
|
||||||
|
array(
|
||||||
|
'type' => 'base_location_country',
|
||||||
|
'value' => 'US',
|
||||||
|
'operation' => '=',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
$remove_plugins_activated_rule = function( $is_visible ) {
|
$remove_plugins_activated_rule = function( $is_visible ) {
|
||||||
$is_visible = array_filter(
|
$is_visible = array_filter(
|
||||||
array_map(
|
array_map(
|
||||||
|
|
|
@ -1641,6 +1641,84 @@ FROM $order_meta_table
|
||||||
|
|
||||||
$changes = $order->get_changes();
|
$changes = $order->get_changes();
|
||||||
$this->update_address_index_meta( $order, $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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,7 +9,9 @@ use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController;
|
||||||
use Automattic\WooCommerce\Internal\Admin\Orders\Edit;
|
use Automattic\WooCommerce\Internal\Admin\Orders\Edit;
|
||||||
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
|
use Automattic\WooCommerce\Internal\Admin\Orders\EditLock;
|
||||||
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
|
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\Admin\Orders\PageController;
|
||||||
|
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
|
||||||
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,6 +30,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
|
||||||
Edit::class,
|
Edit::class,
|
||||||
ListTable::class,
|
ListTable::class,
|
||||||
EditLock::class,
|
EditLock::class,
|
||||||
|
TaxonomiesMetaBox::class,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,5 +44,6 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
|
||||||
$this->share( Edit::class )->addArgument( PageController::class );
|
$this->share( Edit::class )->addArgument( PageController::class );
|
||||||
$this->share( ListTable::class )->addArgument( PageController::class );
|
$this->share( ListTable::class )->addArgument( PageController::class );
|
||||||
$this->share( EditLock::class );
|
$this->share( EditLock::class );
|
||||||
|
$this->share( TaxonomiesMetaBox::class )->addArgument( OrdersTableDataStore::class );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,7 +138,7 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Expand all variations.', async () => {
|
await test.step( 'Expand all variations.', async () => {
|
||||||
|
@ -231,7 +231,7 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Expand all variations.', async () => {
|
await test.step( 'Expand all variations.', async () => {
|
||||||
|
@ -344,7 +344,7 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step(
|
await test.step(
|
||||||
|
@ -387,7 +387,7 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step(
|
await test.step(
|
||||||
|
@ -418,7 +418,7 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Expand all variations', async () => {
|
await test.step( 'Expand all variations', async () => {
|
||||||
|
@ -538,7 +538,11 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
|
} );
|
||||||
|
|
||||||
|
await test.step( 'Wait for block overlay to disappear.', async () => {
|
||||||
|
await expect( page.locator( '.blockOverlay' ) ).not.toBeVisible();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Select variation defaults', async () => {
|
await test.step( 'Select variation defaults', async () => {
|
||||||
|
@ -598,7 +602,7 @@ test.describe( 'Update variations', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click on the "Variations" tab.', async () => {
|
await test.step( 'Click on the "Variations" tab.', async () => {
|
||||||
await page.locator( 'a[href="#variable_product_options"]' ).click();
|
await page.locator( '.variations_tab > a[href="#variable_product_options"]' ).click();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
await test.step( 'Click "Remove" on a variation', async () => {
|
await test.step( 'Click "Remove" on a variation', async () => {
|
||||||
|
|
|
@ -190,7 +190,7 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
|
||||||
*/
|
*/
|
||||||
public function test_apply_coupon_across_status() {
|
public function test_apply_coupon_across_status() {
|
||||||
$coupon_code = 'coupon_test_count_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() );
|
$this->assertEquals( 0, $coupon->get_usage_count() );
|
||||||
|
|
||||||
$order = WC_Helper_Order::create_order();
|
$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() {
|
public function test_apply_coupon_stores_meta_data() {
|
||||||
$coupon_code = 'coupon_test_meta_data';
|
$coupon_code = 'coupon_test_meta_data';
|
||||||
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
|
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
|
||||||
$order = WC_Helper_Order::create_order();
|
$order = WC_Helper_Order::create_order();
|
||||||
$order->set_status( 'processing' );
|
$order->set_status( 'processing' );
|
||||||
$order->save();
|
$order->save();
|
||||||
$order->apply_coupon( $coupon_code );
|
$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() );
|
$order = wc_get_order( $order->get_id() );
|
||||||
$this->assertInstanceOf( Automattic\WooCommerce\Admin\Overrides\Order::class, $order );
|
$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 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,4 +275,22 @@ class WC_Order_Data_Store_CPT_Test extends WC_Unit_Test_Case {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the untrashing an order works as expected when done in an agnostic way (ie, not depending directly on
|
||||||
|
* functions such as `wp_untrash_post()`.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function test_untrash(): void {
|
||||||
|
$order = WC_Helper_Order::create_order();
|
||||||
|
$order_id = $order->get_id();
|
||||||
|
$original_status = $order->get_status();
|
||||||
|
|
||||||
|
$order->delete();
|
||||||
|
$this->assertEquals( 'trash', $order->get_status(), 'The order was successfully trashed.' );
|
||||||
|
|
||||||
|
$order = wc_get_order( $order_id );
|
||||||
|
$this->assertTrue( $order->untrash(), 'The order was restored from the trash.' );
|
||||||
|
$this->assertEquals( $original_status, $order->get_status(), 'The original order status is restored following untrash.' );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,7 @@ class OrdersTableQueryTests extends WC_Unit_Test_Case {
|
||||||
*/
|
*/
|
||||||
public function test_query_filters() {
|
public function test_query_filters() {
|
||||||
$order1 = new \WC_Order();
|
$order1 = new \WC_Order();
|
||||||
|
$order1->set_date_created( time() - HOUR_IN_SECONDS );
|
||||||
$order1->save();
|
$order1->save();
|
||||||
|
|
||||||
$order2 = new \WC_Order();
|
$order2 = new \WC_Order();
|
||||||
|
@ -198,10 +199,10 @@ class OrdersTableQueryTests extends WC_Unit_Test_Case {
|
||||||
$this->assertCount( 0, wc_get_orders( array() ) );
|
$this->assertCount( 0, wc_get_orders( array() ) );
|
||||||
remove_all_filters( 'woocommerce_orders_table_query_clauses' );
|
remove_all_filters( 'woocommerce_orders_table_query_clauses' );
|
||||||
|
|
||||||
// Force a query that sorts orders by id DESC (as opposed to the default date ASC) if a query arg is present.
|
// Force a query that sorts orders by id ASC (as opposed to the default date DESC) if a query arg is present.
|
||||||
$filter_callback = function( $clauses, $query, $query_args ) use ( $order1 ) {
|
$filter_callback = function( $clauses, $query, $query_args ) {
|
||||||
if ( ! empty( $query_args['my_custom_arg'] ) ) {
|
if ( ! empty( $query_args['my_custom_arg'] ) ) {
|
||||||
$clauses['orderby'] = $query->get_table_name( 'orders' ) . '.id DESC';
|
$clauses['orderby'] = $query->get_table_name( 'orders' ) . '.id ASC';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $clauses;
|
return $clauses;
|
||||||
|
@ -211,7 +212,8 @@ class OrdersTableQueryTests extends WC_Unit_Test_Case {
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
wc_get_orders(
|
wc_get_orders(
|
||||||
array(
|
array(
|
||||||
'return' => 'ids',
|
'return' => 'ids',
|
||||||
|
'my_custom_arg' => true,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
|
@ -222,8 +224,7 @@ class OrdersTableQueryTests extends WC_Unit_Test_Case {
|
||||||
$this->assertEquals(
|
$this->assertEquals(
|
||||||
wc_get_orders(
|
wc_get_orders(
|
||||||
array(
|
array(
|
||||||
'return' => 'ids',
|
'return' => 'ids',
|
||||||
'my_custom_arg' => true,
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
|
|
Loading…
Reference in New Issue