Merge branch 'hotfix/info-trailing-semi-colon' of https://github.com/marcodafonseca/woocommerce into hotfix/info-trailing-semi-colon
This commit is contained in:
commit
3cbe99c29a
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Do not remove sale date from when the sale is still active
|
|
@ -0,0 +1,160 @@
|
|||
# Translating WooCommerce
|
||||
|
||||
WooCommerce is already translated into several languages and is translation-ready right out of the box. All that’s needed is a translation file for your language.
|
||||
|
||||
There are several methods to create a translation, most of which are outlined in the WordPress Codex. In most cases you can contribute to the project on [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/).
|
||||
|
||||
To create custom translations you can consider using [Poedit](https://poedit.net/).
|
||||
|
||||
## Set up WordPress in your language
|
||||
|
||||
To set your WordPress site's language:
|
||||
|
||||
1. Go to `WP Admin » Settings » General` and adjust the `Site Language`.
|
||||
2. Go to `WP Admin » Dashboard » Updates` and click the `Update Translations` button.
|
||||
|
||||
Once this has been done, the shop displays in your locale if the language file exists. Otherwise, you need to create the language files (process explained below).
|
||||
|
||||
## Contributing your localization to core
|
||||
|
||||
We encourage contributions to our translations. If you want to add translated strings or start a new translation, simply register at WordPress.org and submit your translations to [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/) for approval.
|
||||
|
||||
## Translating WooCommerce into your language
|
||||
|
||||
Both stable and development versions of WooCommerce are available for translation. When you install or update WooCommerce, WordPress will automatically fetch a 100% complete translation for your language. If such a translation isn't available, you can either download it manually or contribute to complete the translation, benefiting all users.
|
||||
|
||||
If you’re new to translating, check out the [translators handbook](https://make.wordpress.org/polyglots/handbook/tools/glotpress-translate-wordpress-org/) to get started.
|
||||
|
||||
### Downloading translations from translate.wordpress.org manually
|
||||
|
||||
1. Go to [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/woocommerce) and look for your language in the list.
|
||||
2. Click the title to be taken to the section for that language.
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/2016-02-17-at-09.57.png)
|
||||
|
||||
3. Click the heading under `Set/Sub Project` to view and download a Stable version.
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/2016-02-17-at-09.59.png)
|
||||
|
||||
4. Scroll to the bottom for export options. Export a `.mo` file for use on your site.
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/2016-02-17-at-10.00.png)
|
||||
|
||||
5. Rename this file to `woocommerce-YOURLANG.mo` (e.g., Great Britain English should be `en_GB`). The corresponding language code can be found by going to [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/) and opening the desired language. The language code is visible in the upper-right corner.
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-17-at-09.44.53.png)
|
||||
|
||||
6. Upload to your site under `wp-content/languages/woocommerce/`. Once uploaded, this translation file may be used.
|
||||
|
||||
## Creating custom translations
|
||||
|
||||
WooCommerce includes a language file (`.pot` file) that contains all of the English text. You can find this language file inside the plugin folder in `woocommerce/i18n/languages/`.
|
||||
|
||||
## Creating custom translations with PoEdit
|
||||
|
||||
WooCommerce comes with a `.pot` file that can be imported into PoEdit to translate.
|
||||
|
||||
To get started:
|
||||
|
||||
1. Open PoEdit and select `Create new translation from POT template`.
|
||||
2. Choose `woocommerce.pot` and PoEdit will show the catalog properties window.
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/Screen-Shot-2013-05-09-at-10.16.46.png)
|
||||
|
||||
3. Enter your name and details, so other translators know who you are, and click `OK`.
|
||||
4. Save your `.po` file. Name it based on what you are translating to, i.e., a GB translation is saved as `woocommerce-en_GB.po`. Now the strings are listed.
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/Screen-Shot-2013-05-09-at-10.20.58.png)
|
||||
|
||||
5. Save after translating strings. The `.mo` file is generated automatically.
|
||||
6. Update your `.po` file by opening it and then go to `Catalog » Update from POT file`.
|
||||
7. Choose the file and it will be updated accordingly.
|
||||
|
||||
## Making your translation upgrade safe
|
||||
|
||||
> **Note:** We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/). If you need to further customize a snippet, or extend its functionality, we highly recommend [Codeable](https://codeable.io/?ref=z4Hnp), or a [Certified WooExpert](https://woocommerce.com/experts/).
|
||||
|
||||
WooCommerce keeps translations in `wp-content/languages/plugins`, like all other plugins. But if you wish to include a custom translation, you can use the directory `wp-content/languages/woocommerce`, or you can use a snippet to load a custom translation stored elsewhere:
|
||||
|
||||
```php
|
||||
// Code to be placed in functions.php of your theme or a custom plugin file.
|
||||
add_filter( 'load_textdomain_mofile', 'load_custom_plugin_translation_file', 10, 2 );
|
||||
|
||||
/**
|
||||
* Replace 'textdomain' with your plugin's textdomain. e.g. 'woocommerce'.
|
||||
* File to be named, for example, yourtranslationfile-en_GB.mo
|
||||
* File to be placed, for example, wp-content/languages/textdomain/yourtranslationfile-en_GB.mo
|
||||
*/
|
||||
function load_custom_plugin_translation_file( $mofile, $domain ) {
|
||||
if ( 'textdomain' === $domain ) {
|
||||
$mofile = WP_LANG_DIR . '/textdomain/yourtranslationfile-' . get_locale() . '.mo';
|
||||
}
|
||||
|
||||
return $mofile;
|
||||
}
|
||||
```
|
||||
|
||||
## Other tools
|
||||
|
||||
There are some other third-party tools that can help with translations. The following list shows a few of them.
|
||||
|
||||
### Loco Translate
|
||||
|
||||
[Loco Translate](https://wordpress.org/plugins/loco-translate/) provides in-browser editing of WordPress translation files and integration with automatic translation services.
|
||||
|
||||
### Say what?
|
||||
|
||||
[Say what?](https://wordpress.org/plugins/say-what/) allows to effortlessly translate or modify specific words without delving into a WordPress theme's `.po` file.
|
||||
|
||||
### String locator
|
||||
|
||||
[String Locator](https://wordpress.org/plugins/string-locator/) enables quick searches within themes, plugins, or the WordPress core, displaying a list of files with the matching text and its line number.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why some strings on the Checkout page are not being translated?
|
||||
|
||||
You may see that some of the strings are not being translated on the Checkout page. For example, in the screenshot below, `Local pickup` shipping method, `Cash on delivery` payment method and a message related to Privacy Policy are not being translated to Russian while the rest of the form is indeed translated:
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2012/01/not_translated.jpg)
|
||||
|
||||
This usually happens when you first install WooCommerce and select default site language (English) and later change the site language to another one. In WooCommerce, the strings that have not been translated in the screenshot, are stored in the database after the initial WooCommerce installation. Therefore, if the site language is changed to another one, there is no way for WooCommerce to detect a translatable string since these are database entries.
|
||||
|
||||
In order to fix it, navigate to WooCommerce settings corresponding to the string you need to change and update the translation there directly. For example, to fix the strings in our case above, you would need to do the following:
|
||||
|
||||
**Local pickup**:
|
||||
|
||||
1. Go to `WooCommerce » Settings » Shipping » Shipping Zones`.
|
||||
2. Select the shipping zone where "Local pickup" is listed.
|
||||
3. Open "Local pickup" settings.
|
||||
4. Rename the method using your translation.
|
||||
5. Save the setting.
|
||||
|
||||
**Cash on delivery**:
|
||||
|
||||
1. Go to `WooCommerce » Settings » Payments`.
|
||||
2. Select the "Cash on delivery" payment method.
|
||||
3. Open its settings.
|
||||
4. Rename the method title, description, and instructions using your translation.
|
||||
5. Save the setting.
|
||||
|
||||
**Privacy policy message**:
|
||||
|
||||
1. Go to `WooCommerce » Settings » Accounts & Privacy`.
|
||||
2. Scroll to the "Privacy policy" section.
|
||||
3. Edit both the `Registration privacy policy` and `Checkout privacy policy` fields with your translation.
|
||||
4. Save the settings.
|
||||
|
||||
Navigate back to the Checkout page – translations should be reflected there.
|
||||
|
||||
### I have translated the strings I needed, but some of them don’t show up translated on the front end. Why?
|
||||
|
||||
If some of your translated strings don’t show up as expected on your WooCommerce site, the first thing to check is if these strings have both a Single and Plural form in the Source text section. To do so, open the corresponding translation on [https://translate.wordpress.org/projects/wp-plugins/woocommerce/](https://translate.wordpress.org/projects/wp-plugins/woocommerce/), e.g. [the translation for Product and Products](https://translate.wordpress.org/projects/wp-plugins/woocommerce/stable/de/default/?filters%5Bstatus%5D=either&filters%5Boriginal_id%5D=577764&filters%5Btranslation_id%5D=24210880).
|
||||
|
||||
This screenshot shows that the Singular translation is available:
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-17-at-10.10.06.png)
|
||||
|
||||
While this screenshot shows that the Plural translation is not available:
|
||||
|
||||
![screenshot](https://woocommerce.com/wp-content/uploads/2023/10/Screenshot-2023-10-17-at-10.10.21.png)
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
Comment: Just updating dependencies in prep for a future PR (to figure out why these are breaking the build).
|
||||
|
||||
|
|
@ -28,8 +28,11 @@
|
|||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@woocommerce/expression-evaluation": "workspace:*",
|
||||
"@wordpress/block-editor": "^9.8.0",
|
||||
"@wordpress/blocks": "^12.3.0"
|
||||
"@wordpress/blocks": "^12.3.0",
|
||||
"@wordpress/data": "wp-6.0",
|
||||
"@wordpress/element": "wp-6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
|
@ -37,6 +40,7 @@
|
|||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/testing-library__jest-dom": "^5.14.3",
|
||||
"@types/wordpress__block-editor": "^7.0.0",
|
||||
"@types/wordpress__blocks": "^11.0.7",
|
||||
|
@ -51,6 +55,8 @@
|
|||
"jest-cli": "^27.5.1",
|
||||
"postcss": "^8.4.7",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass-loader": "^10.2.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
|
@ -73,5 +79,10 @@
|
|||
"start": "concurrently \"tsc --project tsconfig.json --watch\" \"tsc --project tsconfig-cjs.json --watch\" \"webpack --watch\"",
|
||||
"prepack": "pnpm run clean && pnpm run build",
|
||||
"lint:fix": "eslint src --fix"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Added shouldLoop prop for the Loader component to determine if looping should happen
|
|
@ -118,19 +118,30 @@ Loader.Subtext = ( {
|
|||
|
||||
const LoaderSequence = ( {
|
||||
interval,
|
||||
shouldLoop = true,
|
||||
children,
|
||||
}: { interval: number } & withReactChildren ) => {
|
||||
}: { interval: number; shouldLoop?: boolean } & withReactChildren ) => {
|
||||
const [ index, setIndex ] = useState( 0 );
|
||||
const childCount = Children.count( children );
|
||||
|
||||
useEffect( () => {
|
||||
const rotateInterval = setInterval( () => {
|
||||
setIndex(
|
||||
( prevIndex ) => ( prevIndex + 1 ) % Children.count( children )
|
||||
);
|
||||
setIndex( ( prevIndex ) => {
|
||||
const nextIndex = prevIndex + 1;
|
||||
|
||||
if ( shouldLoop ) {
|
||||
return nextIndex % childCount;
|
||||
}
|
||||
if ( nextIndex < childCount ) {
|
||||
return nextIndex;
|
||||
}
|
||||
clearInterval( rotateInterval );
|
||||
return prevIndex;
|
||||
} );
|
||||
}, interval );
|
||||
|
||||
return () => clearInterval( rotateInterval );
|
||||
}, [ interval, children ] );
|
||||
}, [ interval, children, shouldLoop, childCount ] );
|
||||
|
||||
const childToDisplay = Children.toArray( children )[ index ];
|
||||
return <>{ childToDisplay }</>;
|
||||
|
|
|
@ -29,8 +29,28 @@ export const ExampleSimpleLoader = () => (
|
|||
</Loader>
|
||||
);
|
||||
|
||||
export const ExampleNonLoopingLoader = () => (
|
||||
<Loader>
|
||||
<Loader.Layout>
|
||||
<Loader.Illustration>
|
||||
<img
|
||||
src="https://placekitten.com/200/200"
|
||||
alt="a cute kitteh"
|
||||
/>
|
||||
</Loader.Illustration>
|
||||
<Loader.Title>Very Impressive Title</Loader.Title>
|
||||
<Loader.ProgressBar progress={ 30 } />
|
||||
<Loader.Sequence interval={ 1000 } shouldLoop={ false }>
|
||||
<Loader.Subtext>Message 1</Loader.Subtext>
|
||||
<Loader.Subtext>Message 2</Loader.Subtext>
|
||||
<Loader.Subtext>Message 3</Loader.Subtext>
|
||||
</Loader.Sequence>
|
||||
</Loader.Layout>
|
||||
</Loader>
|
||||
);
|
||||
|
||||
/** <Loader> component story with controls */
|
||||
const Template = ( { progress, title, messages } ) => (
|
||||
const Template = ( { progress, title, messages, shouldLoop } ) => (
|
||||
<Loader>
|
||||
<Loader.Layout>
|
||||
<Loader.Illustration>
|
||||
|
@ -41,7 +61,7 @@ const Template = ( { progress, title, messages } ) => (
|
|||
</Loader.Illustration>
|
||||
<Loader.Title>{ title }</Loader.Title>
|
||||
<Loader.ProgressBar progress={ progress } />
|
||||
<Loader.Sequence interval={ 1000 }>
|
||||
<Loader.Sequence interval={ 1000 } shouldLoop={ shouldLoop }>
|
||||
{ messages.map( ( message, index ) => (
|
||||
<Loader.Subtext key={ index }>{ message }</Loader.Subtext>
|
||||
) ) }
|
||||
|
@ -54,6 +74,7 @@ export const ExampleLoaderWithControls = Template.bind( {} );
|
|||
ExampleLoaderWithControls.args = {
|
||||
title: 'Very Impressive Title',
|
||||
progress: 30,
|
||||
shouldLoop: true,
|
||||
messages: [ 'Message 1', 'Message 2', 'Message 3' ],
|
||||
};
|
||||
|
||||
|
@ -71,6 +92,9 @@ export default {
|
|||
max: 100,
|
||||
},
|
||||
},
|
||||
shouldLoop: {
|
||||
control: 'boolean',
|
||||
},
|
||||
messages: {
|
||||
control: 'object',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Create ManageDownloadLimitsModal component
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Let the toogle block to use inverted value to be checked
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Remove use of :has() css selector as it is not compatible with FireFox.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Remove empty values from the shipping class request so slug can be generated server side
|
|
@ -22,6 +22,12 @@
|
|||
"disabledCopy": {
|
||||
"type": "string",
|
||||
"__experimentalRole": "content"
|
||||
},
|
||||
"checkedValue": {
|
||||
"type": "object"
|
||||
},
|
||||
"uncheckedValue": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
|
|
|
@ -18,19 +18,41 @@ export function Edit( {
|
|||
context: { postType },
|
||||
}: ProductEditorBlockEditProps< ToggleBlockAttributes > ) {
|
||||
const blockProps = useWooBlockProps( attributes );
|
||||
const { label, property, disabled, disabledCopy } = attributes;
|
||||
const {
|
||||
label,
|
||||
property,
|
||||
disabled,
|
||||
disabledCopy,
|
||||
checkedValue,
|
||||
uncheckedValue,
|
||||
} = attributes;
|
||||
const [ value, setValue ] = useProductEntityProp< boolean >( property, {
|
||||
postType,
|
||||
fallbackValue: false,
|
||||
} );
|
||||
|
||||
function isChecked() {
|
||||
if ( checkedValue !== undefined ) {
|
||||
return checkedValue === value;
|
||||
}
|
||||
return value as boolean;
|
||||
}
|
||||
|
||||
function handleChange( checked: boolean ) {
|
||||
if ( checked ) {
|
||||
setValue( checkedValue !== undefined ? checkedValue : checked );
|
||||
} else {
|
||||
setValue( uncheckedValue !== undefined ? uncheckedValue : checked );
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<ToggleControl
|
||||
label={ label }
|
||||
checked={ value }
|
||||
checked={ isChecked() }
|
||||
disabled={ disabled }
|
||||
onChange={ setValue }
|
||||
onChange={ handleChange }
|
||||
/>
|
||||
{ disabled && (
|
||||
<p
|
||||
|
|
|
@ -7,4 +7,6 @@ export interface ToggleBlockAttributes extends BlockAttributes {
|
|||
label: string;
|
||||
property: string;
|
||||
disabled?: boolean;
|
||||
checkedValue?: never;
|
||||
uncheckedValue?: never;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
createElement,
|
||||
Fragment,
|
||||
createInterpolateElement,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { closeSmall } from '@wordpress/icons';
|
||||
import { MediaItem } from '@wordpress/media-utils';
|
||||
|
@ -26,6 +27,10 @@ import { UploadsBlockAttributes } from './types';
|
|||
import { UploadImage } from './upload-image';
|
||||
import { DownloadsMenu } from './downloads-menu';
|
||||
import { ProductEditorBlockEditProps } from '../../../types';
|
||||
import {
|
||||
ManageDownloadLimitsModal,
|
||||
ManageDownloadLimitsModalProps,
|
||||
} from '../../../components/manage-download-limits-modal';
|
||||
|
||||
function getFileName( url?: string ) {
|
||||
const [ name ] = url?.split( '/' ).reverse() ?? [];
|
||||
|
@ -55,6 +60,12 @@ export function Edit( {
|
|||
postType,
|
||||
'downloads'
|
||||
);
|
||||
const [ downloadLimit, setDownloadLimit ] = useEntityProp<
|
||||
Product[ 'download_limit' ]
|
||||
>( 'postType', 'product', 'download_limit' );
|
||||
const [ downloadExpiry, setDownloadExpiry ] = useEntityProp<
|
||||
Product[ 'download_expiry' ]
|
||||
>( 'postType', 'product', 'download_expiry' );
|
||||
|
||||
const { allowedMimeTypes } = useSelect( ( select ) => {
|
||||
const { getEditorSettings } = select( 'core/editor' );
|
||||
|
@ -67,6 +78,25 @@ export function Edit( {
|
|||
|
||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
const [ showManageDownloadLimitsModal, setShowManageDownloadLimitsModal ] =
|
||||
useState( false );
|
||||
|
||||
function handleManageLimitsClick() {
|
||||
setShowManageDownloadLimitsModal( true );
|
||||
}
|
||||
|
||||
function handleManageDownloadLimitsModalClose() {
|
||||
setShowManageDownloadLimitsModal( false );
|
||||
}
|
||||
|
||||
function handleManageDownloadLimitsModalSubmit(
|
||||
value: ManageDownloadLimitsModalProps[ 'initialValue' ]
|
||||
) {
|
||||
setDownloadLimit( value.downloadLimit as number );
|
||||
setDownloadExpiry( value.downloadExpiry as number );
|
||||
setShowManageDownloadLimitsModal( false );
|
||||
}
|
||||
|
||||
function handleFileUpload( files: MediaItem | MediaItem[] ) {
|
||||
if ( ! Array.isArray( files ) ) return;
|
||||
|
||||
|
@ -140,6 +170,15 @@ export function Edit( {
|
|||
return (
|
||||
<div { ...blockProps }>
|
||||
<div className="wp-block-woocommerce-product-downloads-field__header">
|
||||
{ Boolean( downloads.length ) && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={ handleManageLimitsClick }
|
||||
>
|
||||
{ __( 'Manage limits', 'woocommerce' ) }
|
||||
</Button>
|
||||
) }
|
||||
|
||||
<DownloadsMenu
|
||||
allowedTypes={ allowedTypes }
|
||||
onUploadSuccess={ handleFileUpload }
|
||||
|
@ -238,6 +277,14 @@ export function Edit( {
|
|||
</Sortable>
|
||||
) }
|
||||
</div>
|
||||
|
||||
{ showManageDownloadLimitsModal && (
|
||||
<ManageDownloadLimitsModal
|
||||
initialValue={ { downloadLimit, downloadExpiry } }
|
||||
onSubmit={ handleManageDownloadLimitsModalSubmit }
|
||||
onClose={ handleManageDownloadLimitsModalClose }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ $fixed-section-height: 224px;
|
|||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: $grid-unit-30;
|
||||
gap: $grid-unit;
|
||||
}
|
||||
|
||||
&__body {
|
||||
|
|
|
@ -86,7 +86,7 @@ export function Edit( {
|
|||
|
||||
const [ categories ] = useEntityProp< PartialProduct[ 'categories' ] >(
|
||||
'postType',
|
||||
'product',
|
||||
context.postType,
|
||||
'categories'
|
||||
);
|
||||
const [ shippingClass, setShippingClass ] = useEntityProp< string >(
|
||||
|
|
|
@ -111,6 +111,21 @@ export function AddNewShippingClassModal( {
|
|||
onAdd,
|
||||
onCancel,
|
||||
}: AddNewShippingClassModalProps ) {
|
||||
function handleSubmit( values: Partial< ProductShippingClass > ) {
|
||||
return onAdd(
|
||||
Object.entries( values ).reduce( function removeEmptyValue(
|
||||
current,
|
||||
[ name, value ]
|
||||
) {
|
||||
return {
|
||||
...current,
|
||||
[ name ]: value === '' ? undefined : value,
|
||||
};
|
||||
},
|
||||
{} )
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'New shipping class', 'woocommerce' ) }
|
||||
|
@ -121,7 +136,7 @@ export function AddNewShippingClassModal( {
|
|||
initialValues={ shippingClass ?? INITIAL_VALUES }
|
||||
validate={ validateForm }
|
||||
errors={ {} }
|
||||
onSubmit={ onAdd }
|
||||
onSubmit={ handleSubmit }
|
||||
>
|
||||
{ ( childrenProps: {
|
||||
handleSubmit: () => Promise< ProductShippingClass >;
|
||||
|
|
|
@ -145,15 +145,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-layout:has(.woocommerce-product-block-editor) {
|
||||
.woocommerce-layout__primary {
|
||||
margin-top: calc($gap-largest + $gap-smaller);
|
||||
}
|
||||
.woocommerce-layout__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wp-admin.woocommerce-feature-enabled-product-block-editor {
|
||||
.components-modal {
|
||||
&__frame {
|
||||
|
|
|
@ -34,3 +34,8 @@ export * from './add-new-shipping-class-modal';
|
|||
export { VariationSwitcherFooter as __experimentalVariationSwitcherFooter } from './variation-switcher-footer';
|
||||
|
||||
export * from './remove-confirmation-modal';
|
||||
|
||||
export {
|
||||
ManageDownloadLimitsModal as __experimentalManageDownloadLimitsModal,
|
||||
ManageDownloadLimitsModalProps,
|
||||
} from './manage-download-limits-modal';
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './manage-download-limits-modal';
|
||||
export * from './types';
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { FormEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
BaseControl,
|
||||
Button,
|
||||
Modal,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ManageDownloadLimitsModalProps } from './types';
|
||||
import { useNumberInputProps } from '../../hooks/use-number-input-props';
|
||||
|
||||
const DOWNLOAD_LIMIT_MIN = 0;
|
||||
const DOWNLOAD_EXPIRY_MIN = 0;
|
||||
|
||||
/**
|
||||
* Download limit and download expiry currently support
|
||||
* `-1`, `0`/`null` and a positive integer.
|
||||
* When the value is `-1` downloads can be unlimited.
|
||||
* When the value is `0` or `null` downloads are unabled.
|
||||
* When the value is greater then `0` downloads are fixed
|
||||
* to the amount set as value.
|
||||
*
|
||||
* @param value The amount of downloads
|
||||
* @return A valid number as string or empty
|
||||
*/
|
||||
function getInitialValue( value: number | null ): string {
|
||||
if ( value === null ) {
|
||||
return '0';
|
||||
}
|
||||
if ( value === -1 ) {
|
||||
return '';
|
||||
}
|
||||
return String( value );
|
||||
}
|
||||
|
||||
export function ManageDownloadLimitsModal( {
|
||||
initialValue,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: ManageDownloadLimitsModalProps ) {
|
||||
const [ downloadLimit, setDownloadLimit ] = useState< string >(
|
||||
getInitialValue( initialValue.downloadLimit )
|
||||
);
|
||||
const [ downloadExpiry, setDownloadExpiry ] = useState< string >(
|
||||
getInitialValue( initialValue.downloadExpiry )
|
||||
);
|
||||
const [ errors, setErrors ] = useState< Record< string, string > >( {} );
|
||||
|
||||
function validateDownloadLimit() {
|
||||
if ( downloadLimit && ! Number.isInteger( Number( downloadLimit ) ) ) {
|
||||
setErrors( ( current ) => ( {
|
||||
...current,
|
||||
downloadLimit: __(
|
||||
'Download limit must be an integer number',
|
||||
'woocommerce'
|
||||
),
|
||||
} ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Number.parseInt( downloadLimit, 10 ) < DOWNLOAD_LIMIT_MIN ) {
|
||||
setErrors( ( current ) => ( {
|
||||
...current,
|
||||
downloadLimit: sprintf(
|
||||
__(
|
||||
// translators: %d is the minimum value of the number input.
|
||||
'Download limit must be greater than or equal to %d',
|
||||
'woocommerce'
|
||||
),
|
||||
DOWNLOAD_LIMIT_MIN
|
||||
),
|
||||
} ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
setErrors( ( { downloadLimit: _, ...current } ) => current );
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateDownloadExpiry() {
|
||||
if (
|
||||
downloadExpiry &&
|
||||
! Number.isInteger( Number( downloadExpiry ) )
|
||||
) {
|
||||
setErrors( ( current ) => ( {
|
||||
...current,
|
||||
downloadExpiry: __(
|
||||
'Expiry period must be an integer number',
|
||||
'woocommerce'
|
||||
),
|
||||
} ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( Number.parseInt( downloadExpiry, 10 ) < DOWNLOAD_EXPIRY_MIN ) {
|
||||
setErrors( ( current ) => ( {
|
||||
...current,
|
||||
downloadExpiry: sprintf(
|
||||
__(
|
||||
// translators: %d is the minimum value of the number input.
|
||||
'Expiry period must be greater than or equal to %d',
|
||||
'woocommerce'
|
||||
),
|
||||
DOWNLOAD_EXPIRY_MIN
|
||||
),
|
||||
} ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
setErrors( ( { downloadExpiry: _, ...current } ) => current );
|
||||
return true;
|
||||
}
|
||||
|
||||
const downloadLimitProps = {
|
||||
...useNumberInputProps( {
|
||||
value: downloadLimit,
|
||||
onChange: setDownloadLimit,
|
||||
} ),
|
||||
id: useInstanceId(
|
||||
BaseControl,
|
||||
'product_download_limit_field'
|
||||
) as string,
|
||||
type: 'number',
|
||||
min: DOWNLOAD_LIMIT_MIN,
|
||||
className: classNames( {
|
||||
'has-error': errors.downloadLimit,
|
||||
} ),
|
||||
label: __( 'Download limit', 'woocommerce' ),
|
||||
help:
|
||||
errors.downloadLimit ||
|
||||
__(
|
||||
'Decide how many times customers can download files after purchasing the product. Leave blank for unlimited re-downloads.',
|
||||
'woocommerce'
|
||||
),
|
||||
placeholder: __( 'Unlimited', 'woocommerce' ),
|
||||
suffix: (
|
||||
<span className="woocommerce-manage-download-limits-modal__input-suffix">
|
||||
{ __( 'times', 'woocommerce' ) }
|
||||
</span>
|
||||
),
|
||||
onBlur() {
|
||||
validateDownloadLimit();
|
||||
},
|
||||
};
|
||||
|
||||
const downloadExpiryProps = {
|
||||
...useNumberInputProps( {
|
||||
value: downloadExpiry,
|
||||
onChange: setDownloadExpiry,
|
||||
} ),
|
||||
id: useInstanceId(
|
||||
BaseControl,
|
||||
'product_download_expiry_field'
|
||||
) as string,
|
||||
type: 'number',
|
||||
min: DOWNLOAD_EXPIRY_MIN,
|
||||
className: classNames( {
|
||||
'has-error': errors.downloadExpiry,
|
||||
} ),
|
||||
label: __( 'Expiry period', 'woocommerce' ),
|
||||
help:
|
||||
errors.downloadExpiry ||
|
||||
__(
|
||||
'Decide how long customers can access the files after purchasing the product. Leave blank for unlimited access.',
|
||||
'woocommerce'
|
||||
),
|
||||
placeholder: __( 'Unlimited', 'woocommerce' ),
|
||||
suffix: (
|
||||
<span className="woocommerce-manage-download-limits-modal__input-suffix">
|
||||
{ __( 'days', 'woocommerce' ) }
|
||||
</span>
|
||||
),
|
||||
onBlur() {
|
||||
validateDownloadExpiry();
|
||||
},
|
||||
};
|
||||
|
||||
function handleSubmit( event: FormEvent< HTMLFormElement > ) {
|
||||
event.preventDefault();
|
||||
|
||||
const isDownloadLimitValid = validateDownloadLimit();
|
||||
const isDownloadExpiryValid = validateDownloadExpiry();
|
||||
|
||||
if ( isDownloadLimitValid && isDownloadExpiryValid ) {
|
||||
onSubmit( {
|
||||
downloadLimit:
|
||||
downloadLimit === ''
|
||||
? -1
|
||||
: Number.parseInt( downloadLimit, 10 ),
|
||||
downloadExpiry:
|
||||
downloadExpiry === ''
|
||||
? -1
|
||||
: Number.parseInt( downloadExpiry, 10 ),
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelClick() {
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={ __( 'Manage download limits', 'woocommerce' ) }
|
||||
className="woocommerce-manage-download-limits-modal"
|
||||
onRequestClose={ onClose }
|
||||
>
|
||||
<form noValidate onSubmit={ handleSubmit }>
|
||||
<div className="woocommerce-manage-download-limits-modal__content">
|
||||
<InputControl { ...downloadLimitProps } />
|
||||
|
||||
<InputControl { ...downloadExpiryProps } />
|
||||
</div>
|
||||
|
||||
<div className="woocommerce-manage-download-limits-modal__actions">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
type="button"
|
||||
onClick={ handleCancelClick }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
{ __( 'Save', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
.woocommerce-manage-download-limits-modal {
|
||||
@include breakpoint(">600px") {
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
@include breakpoint(">782px") {
|
||||
width: 640px;
|
||||
|
||||
.components-input-control__container {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-suffix {
|
||||
margin-right: $grid-unit-15;
|
||||
}
|
||||
|
||||
&__content {
|
||||
.components-base-control {
|
||||
.components-input-control__container {
|
||||
.components-input-control__input {
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-base-control.has-error {
|
||||
.components-input-control__backdrop {
|
||||
border-color: $studio-red-50;
|
||||
}
|
||||
|
||||
.components-base-control__help {
|
||||
color: $studio-red-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
margin-top: $grid-unit-30;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export type PartialEntity = {
|
||||
downloadLimit: number | null;
|
||||
downloadExpiry: number | null;
|
||||
};
|
||||
|
||||
export type ManageDownloadLimitsModalProps = {
|
||||
initialValue: PartialEntity;
|
||||
onSubmit( value: PartialEntity ): void;
|
||||
onClose(): void;
|
||||
};
|
|
@ -32,6 +32,7 @@
|
|||
@import "components/tags-field/style.scss";
|
||||
@import "components/variation-switcher-footer/variation-switcher-footer.scss";
|
||||
@import "components/remove-confirmation-modal/style.scss";
|
||||
@import "components/manage-download-limits-modal/style.scss";
|
||||
|
||||
/* Field Blocks */
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ export const ApiCallLoader = () => {
|
|||
|
||||
return (
|
||||
<Loader>
|
||||
<Loader.Sequence interval={ 3000 }>
|
||||
<Loader.Sequence interval={ 3500 } shouldLoop={ false }>
|
||||
{ loaderSteps.map( ( step, index ) => (
|
||||
<Loader.Layout key={ index }>
|
||||
<Loader.Illustration>
|
||||
|
|
|
@ -18,15 +18,53 @@ export const fetchThemeCards = async () => {
|
|||
};
|
||||
|
||||
export const fetchIntroData = async () => {
|
||||
let currentThemeIsAiGenerated = false;
|
||||
const currentTemplate = await resolveSelect(
|
||||
coreStore
|
||||
const currentTemplatePromise =
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
).__experimentalGetTemplateForLink( '/' );
|
||||
const maybePreviousTemplate = await resolveSelect(
|
||||
resolveSelect( coreStore ).__experimentalGetTemplateForLink( '/' );
|
||||
|
||||
const maybePreviousTemplatePromise = resolveSelect(
|
||||
OPTIONS_STORE_NAME
|
||||
).getOption( 'woocommerce_admin_customize_store_completed_theme_id' );
|
||||
|
||||
const styleRevsPromise =
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
resolveSelect( coreStore ).getCurrentThemeGlobalStylesRevisions();
|
||||
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
const hasModifiedPagesPromise = resolveSelect( coreStore ).getEntityRecords(
|
||||
'postType',
|
||||
'page',
|
||||
{
|
||||
per_page: 100,
|
||||
_fields: [ 'id', '_links.version-history' ],
|
||||
orderby: 'menu_order',
|
||||
order: 'asc',
|
||||
}
|
||||
);
|
||||
|
||||
const getTaskPromise = resolveSelect( ONBOARDING_STORE_NAME ).getTask(
|
||||
'customize-store'
|
||||
);
|
||||
|
||||
const themeDataPromise = fetchThemeCards();
|
||||
|
||||
const [
|
||||
currentTemplate,
|
||||
maybePreviousTemplate,
|
||||
styleRevs,
|
||||
rawPages,
|
||||
task,
|
||||
themeData,
|
||||
] = await Promise.all( [
|
||||
currentTemplatePromise,
|
||||
maybePreviousTemplatePromise,
|
||||
styleRevsPromise,
|
||||
hasModifiedPagesPromise,
|
||||
getTaskPromise,
|
||||
themeDataPromise,
|
||||
] );
|
||||
|
||||
let currentThemeIsAiGenerated = false;
|
||||
if (
|
||||
maybePreviousTemplate &&
|
||||
currentTemplate?.id === maybePreviousTemplate
|
||||
|
@ -34,33 +72,18 @@ export const fetchIntroData = async () => {
|
|||
currentThemeIsAiGenerated = true;
|
||||
}
|
||||
|
||||
const styleRevs = await resolveSelect(
|
||||
coreStore
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
).getCurrentThemeGlobalStylesRevisions();
|
||||
|
||||
const hasModifiedPages = (
|
||||
await resolveSelect( coreStore )
|
||||
// @ts-expect-error No types for this exist yet.
|
||||
.getEntityRecords( 'postType', 'page', {
|
||||
per_page: 100,
|
||||
_fields: [ 'id', '_links.version-history' ],
|
||||
orderby: 'menu_order',
|
||||
order: 'asc',
|
||||
} )
|
||||
)?.some( ( page: { _links: { [ key: string ]: string[] } } ) => {
|
||||
return page._links?.[ 'version-history' ]?.length > 1;
|
||||
} );
|
||||
|
||||
const { getTask } = resolveSelect( ONBOARDING_STORE_NAME );
|
||||
const hasModifiedPages = rawPages?.some(
|
||||
( page: { _links: { [ key: string ]: string[] } } ) => {
|
||||
return page._links?.[ 'version-history' ]?.length > 1;
|
||||
}
|
||||
);
|
||||
|
||||
const activeThemeHasMods =
|
||||
!! currentTemplate?.modified ||
|
||||
styleRevs?.length > 0 ||
|
||||
hasModifiedPages;
|
||||
const customizeStoreTaskCompleted = ( await getTask( 'customize-store' ) )
|
||||
?.isComplete;
|
||||
const themeData = await fetchThemeCards();
|
||||
|
||||
const customizeStoreTaskCompleted = task?.isComplete;
|
||||
|
||||
return {
|
||||
activeThemeHasMods,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
|
||||
export const MobileAppInstallationInfo = () => {
|
||||
|
@ -13,12 +12,6 @@ export const MobileAppInstallationInfo = () => {
|
|||
}
|
||||
size={ 140 }
|
||||
/>
|
||||
<p>
|
||||
{ __(
|
||||
'Scan the code above to download the WooCommerce mobile app, or visit woocommerce.com/mobile from your mobile device.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import React from '@wordpress/element';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Link } from '@woocommerce/components';
|
||||
|
||||
export const MobileAppLoginInfo = ( {
|
||||
loginUrl,
|
||||
}: {
|
||||
loginUrl: string | undefined;
|
||||
} ) => {
|
||||
return (
|
||||
<div>
|
||||
{ loginUrl && (
|
||||
<div>
|
||||
<QRCodeSVG value={ loginUrl } size={ 140 } />
|
||||
<p>
|
||||
{ __(
|
||||
'The app version needs to be 15.7 or above to sign in with this link.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
) }
|
||||
<div>
|
||||
{ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Any troubles signing in? Check out the {{link}}FAQ{{/link}}.',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href="https://woocommerce.com/document/android-ios-apps-login-help-faq/"
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'onboarding_app_login_faq_click'
|
||||
);
|
||||
} }
|
||||
/>
|
||||
),
|
||||
strong: <strong />,
|
||||
},
|
||||
} ) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React, { useState, useEffect } from '@wordpress/element';
|
||||
import { Button } from '@wordpress/components';
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import { Stepper, StepperProps } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SendMagicLinkButton, SendMagicLinkStates } from './';
|
||||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
import { MobileAppInstallationInfo } from '../components/MobileAppInstallationInfo';
|
||||
import { MobileAppLoginInfo } from '../components/MobileAppLoginInfo';
|
||||
|
||||
export const MobileAppLoginStepper = ( {
|
||||
step,
|
||||
isJetpackPluginInstalled,
|
||||
wordpressAccountEmailAddress,
|
||||
completeInstallationStepHandler,
|
||||
sendMagicLinkHandler,
|
||||
sendMagicLinkStatus,
|
||||
}: {
|
||||
step: 'first' | 'second';
|
||||
isJetpackPluginInstalled: boolean;
|
||||
wordpressAccountEmailAddress: string | undefined;
|
||||
completeInstallationStepHandler: () => void;
|
||||
sendMagicLinkHandler: () => void;
|
||||
sendMagicLinkStatus: SendMagicLinkStates;
|
||||
} ) => {
|
||||
const [ stepsToDisplay, setStepsToDisplay ] = useState<
|
||||
StepperProps[ 'steps' ] | undefined
|
||||
>( undefined );
|
||||
// we need to generate one set of steps for the first step, and another set for the second step
|
||||
// because the texts are different after progressing from the first step to the second step
|
||||
useEffect( () => {
|
||||
if ( step === 'first' ) {
|
||||
setStepsToDisplay( [
|
||||
{
|
||||
key: 'first',
|
||||
label: __( 'Install the mobile app', 'woocommerce' ),
|
||||
description: __(
|
||||
'Scan the code below to download or upgrade the app, or visit woocommerce.com/mobile from your mobile device.',
|
||||
'woocommerce'
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<MobileAppInstallationInfo />
|
||||
<Button
|
||||
variant="primary"
|
||||
className="install-app-button"
|
||||
onClick={ () => {
|
||||
completeInstallationStepHandler();
|
||||
} }
|
||||
>
|
||||
{ __( 'App is installed', 'woocommerce' ) }
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'second',
|
||||
label: __( 'Sign into the app', 'woocommerce' ),
|
||||
description: '',
|
||||
content: <></>,
|
||||
},
|
||||
] );
|
||||
} else if ( step === 'second' ) {
|
||||
if (
|
||||
isJetpackPluginInstalled &&
|
||||
wordpressAccountEmailAddress !== undefined
|
||||
) {
|
||||
setStepsToDisplay( [
|
||||
{
|
||||
key: 'first',
|
||||
label: __( 'App installed', 'woocommerce' ),
|
||||
description: '',
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
key: 'second',
|
||||
label: 'Sign into the app',
|
||||
description: sprintf(
|
||||
/* translators: Reflecting to the user that the magic link has been sent to their WordPress account email address */
|
||||
__(
|
||||
'We’ll send a magic link to %s. Open it on your smartphone or tablet to sign into your store instantly.',
|
||||
'woocommerce'
|
||||
),
|
||||
wordpressAccountEmailAddress
|
||||
),
|
||||
content: (
|
||||
<SendMagicLinkButton
|
||||
isFetching={
|
||||
sendMagicLinkStatus ===
|
||||
SendMagicLinkStates.FETCHING
|
||||
}
|
||||
onClickHandler={ sendMagicLinkHandler }
|
||||
/>
|
||||
),
|
||||
},
|
||||
] );
|
||||
} else {
|
||||
const siteUrl: string = getAdminSetting( 'siteUrl' );
|
||||
const username = getAdminSetting( 'currentUserData' ).username;
|
||||
const loginUrl = `woocommerce://app-login?siteUrl=${ encodeURIComponent(
|
||||
siteUrl
|
||||
) }&username=${ encodeURIComponent( username ) }`;
|
||||
const description = loginUrl
|
||||
? __(
|
||||
'Scan the QR code below and enter the wp-admin password in the app.',
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
'Follow the instructions in the app to sign in.',
|
||||
'woocommerce'
|
||||
);
|
||||
setStepsToDisplay( [
|
||||
{
|
||||
key: 'first',
|
||||
label: __( 'App installed', 'woocommerce' ),
|
||||
description: '',
|
||||
content: <></>,
|
||||
},
|
||||
{
|
||||
key: 'second',
|
||||
label: 'Sign into the app',
|
||||
description,
|
||||
content: <MobileAppLoginInfo loginUrl={ loginUrl } />,
|
||||
},
|
||||
] );
|
||||
}
|
||||
}
|
||||
}, [
|
||||
step,
|
||||
isJetpackPluginInstalled,
|
||||
wordpressAccountEmailAddress,
|
||||
completeInstallationStepHandler,
|
||||
sendMagicLinkHandler,
|
||||
sendMagicLinkStatus,
|
||||
] );
|
||||
|
||||
return (
|
||||
<div className="login-stepper-wrapper">
|
||||
{ stepsToDisplay && (
|
||||
<Stepper
|
||||
isVertical={ true }
|
||||
currentStep={ step }
|
||||
steps={ stepsToDisplay }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -23,11 +23,7 @@ import {
|
|||
useSendMagicLink,
|
||||
SendMagicLinkStates,
|
||||
} from './components';
|
||||
import {
|
||||
EmailSentPage,
|
||||
MobileAppInstallPage,
|
||||
JetpackAlreadyInstalledPage,
|
||||
} from './pages';
|
||||
import { EmailSentPage, MobileAppLoginStepperPage } from './pages';
|
||||
import './style.scss';
|
||||
import { SETUP_TASK_HELP_ITEMS_FILTER } from '../../activity-panel/panels/help';
|
||||
|
||||
|
@ -54,6 +50,7 @@ export const MobileAppModal = () => {
|
|||
}
|
||||
}, [ searchParams ] );
|
||||
|
||||
const [ appInstalledClicked, setAppInstalledClicked ] = useState( false );
|
||||
const [ hasSentEmail, setHasSentEmail ] = useState( false );
|
||||
const [ isRetryingMagicLinkSend, setIsRetryingMagicLinkSend ] =
|
||||
useState( false );
|
||||
|
@ -61,6 +58,11 @@ export const MobileAppModal = () => {
|
|||
const { requestState: magicLinkRequestStatus, fetchMagicLinkApiCall } =
|
||||
useSendMagicLink();
|
||||
|
||||
const completeAppInstallationStep = useCallback( () => {
|
||||
setAppInstalledClicked( true );
|
||||
recordEvent( 'onboarding_app_install_click' );
|
||||
}, [] );
|
||||
|
||||
const sendMagicLink = useCallback( () => {
|
||||
fetchMagicLinkApiCall();
|
||||
recordEvent( 'magic_prompt_send_signin_link_click' );
|
||||
|
@ -83,28 +85,29 @@ export const MobileAppModal = () => {
|
|||
} }
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
state === JetpackPluginStates.FULL_CONNECTION &&
|
||||
jetpackConnectionData?.currentUser?.wpcomUser?.email &&
|
||||
! hasSentEmail
|
||||
) {
|
||||
} else {
|
||||
const isJetpackPluginInstalled =
|
||||
( state === JetpackPluginStates.FULL_CONNECTION &&
|
||||
jetpackConnectionData?.currentUser?.wpcomUser?.email !==
|
||||
undefined ) ??
|
||||
false;
|
||||
const wordpressAccountEmailAddress =
|
||||
jetpackConnectionData?.currentUser.wpcomUser.email;
|
||||
jetpackConnectionData?.currentUser?.wpcomUser?.email;
|
||||
setPageContent(
|
||||
<JetpackAlreadyInstalledPage
|
||||
<MobileAppLoginStepperPage
|
||||
appInstalledClicked={ appInstalledClicked }
|
||||
isJetpackPluginInstalled={ isJetpackPluginInstalled }
|
||||
wordpressAccountEmailAddress={
|
||||
wordpressAccountEmailAddress
|
||||
}
|
||||
isRetryingMagicLinkSend={ isRetryingMagicLinkSend }
|
||||
sendMagicLinkStatus={ magicLinkRequestStatus }
|
||||
completeInstallationHandler={ completeAppInstallationStep }
|
||||
sendMagicLinkHandler={ sendMagicLink }
|
||||
sendMagicLinkStatus={ magicLinkRequestStatus }
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Shows the installation page by default.
|
||||
setPageContent( <MobileAppInstallPage /> );
|
||||
}
|
||||
}, [
|
||||
appInstalledClicked,
|
||||
sendMagicLink,
|
||||
hasSentEmail,
|
||||
isReturningFromWordpressConnection,
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ModalContentLayoutWithTitle } from '../layouts/ModalContentLayoutWithTitle';
|
||||
import { SendMagicLinkStates } from '../components';
|
||||
import { MobileAppLoginStepper } from '../components/MobileAppLoginStepper';
|
||||
|
||||
interface MobileAppLoginStepperPageProps {
|
||||
appInstalledClicked: boolean;
|
||||
isJetpackPluginInstalled: boolean;
|
||||
wordpressAccountEmailAddress: string | undefined;
|
||||
completeInstallationHandler: () => void;
|
||||
sendMagicLinkHandler: () => void;
|
||||
sendMagicLinkStatus: SendMagicLinkStates;
|
||||
}
|
||||
|
||||
export const MobileAppLoginStepperPage: React.FC<
|
||||
MobileAppLoginStepperPageProps
|
||||
> = ( {
|
||||
appInstalledClicked,
|
||||
isJetpackPluginInstalled,
|
||||
wordpressAccountEmailAddress,
|
||||
completeInstallationHandler,
|
||||
sendMagicLinkHandler,
|
||||
sendMagicLinkStatus,
|
||||
} ) => (
|
||||
<ModalContentLayoutWithTitle>
|
||||
<div className="modal-subheader">
|
||||
<h3>
|
||||
{ __(
|
||||
'Run your store from anywhere in two easy steps.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</h3>
|
||||
</div>
|
||||
<MobileAppLoginStepper
|
||||
step={ appInstalledClicked ? 'second' : 'first' }
|
||||
isJetpackPluginInstalled={ isJetpackPluginInstalled }
|
||||
wordpressAccountEmailAddress={ wordpressAccountEmailAddress }
|
||||
completeInstallationStepHandler={ completeInstallationHandler }
|
||||
sendMagicLinkHandler={ sendMagicLinkHandler }
|
||||
sendMagicLinkStatus={ sendMagicLinkStatus }
|
||||
/>
|
||||
</ModalContentLayoutWithTitle>
|
||||
);
|
|
@ -1,4 +1,4 @@
|
|||
export { EmailSentPage } from './EmailSentPage';
|
||||
export { JetpackAlreadyInstalledPage } from './JetpackAlreadyInstalledPage';
|
||||
export { JetpackInstallStepperPage } from './JetpackInstallStepperPage';
|
||||
export { MobileAppInstallPage } from './MobileAppInstallPage';
|
||||
export { MobileAppLoginStepperPage } from './MobileAppLoginStepperPage';
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
max-width: 964px;
|
||||
min-width: 534px;
|
||||
// Overrides the default modal max-height.
|
||||
max-height: 663px;
|
||||
max-height: unset;
|
||||
|
||||
.components-modal__header {
|
||||
height: 0;
|
||||
|
@ -154,7 +154,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-stepper.is-vertical .woocommerce-stepper__step.is-complete {
|
||||
.login-stepper-wrapper {
|
||||
button.install-app-button {
|
||||
position: relative;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-stepper.is-vertical .woocommerce-stepper__step {
|
||||
// default vertical step distance is 36px which is too much and doesn't match the design
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
|
|
@ -280,6 +280,9 @@ export const getPages = () => {
|
|||
navArgs: {
|
||||
id: 'woocommerce-edit-product',
|
||||
},
|
||||
layout: {
|
||||
header: false,
|
||||
},
|
||||
wpOpenMenu: 'menu-posts-product',
|
||||
capability: 'edit_products',
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Do not remove sale date from when the sale is still active
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add virtual section to the product variation template
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Use the Script API strategy feature to defer front-end scripts in WordPress 6.3+
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Added aria-label to breadcrumb element
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Add e2e test for order notes
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Add e2e test to bulk update order statuses
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Update order status to cancelled
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
fix - Fatal error in class-wc-helper-updater.php when transient parameter is null
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Disable the rendering of the header on the variation edit page, as it has its own header.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix duplicate description when editing the product summary
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix cys loading screep should not be looping
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Parallelised the independent network calls on the intro screen so that they become much faster
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
two steps app onboarding
|
|
@ -68,17 +68,21 @@ class WC_Helper_Updater {
|
|||
$item['package'] = 'woocommerce-com-expired-' . $plugin['_product_id'];
|
||||
}
|
||||
|
||||
if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) {
|
||||
$transient->response[ $filename ] = (object) $item;
|
||||
unset( $transient->no_update[ $filename ] );
|
||||
} else {
|
||||
$transient->no_update[ $filename ] = (object) $item;
|
||||
unset( $transient->response[ $filename ] );
|
||||
if ( $transient instanceof stdClass ) {
|
||||
if ( version_compare( $plugin['Version'], $data['version'], '<' ) ) {
|
||||
$transient->response[ $filename ] = (object) $item;
|
||||
unset( $transient->no_update[ $filename ] );
|
||||
} else {
|
||||
$transient->no_update[ $filename ] = (object) $item;
|
||||
unset( $transient->response[ $filename ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$translations = self::get_translations_update_data();
|
||||
$transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations );
|
||||
if ( $transient instanceof stdClass ) {
|
||||
$translations = self::get_translations_update_data();
|
||||
$transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations );
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ class WC_Frontend_Scripts {
|
|||
* @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
|
||||
* @param boolean $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
|
||||
*/
|
||||
private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) {
|
||||
private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = array( 'strategy' => 'defer' ) ) {
|
||||
self::$scripts[] = $handle;
|
||||
wp_register_script( $handle, $path, $deps, $version, $in_footer );
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ class WC_Frontend_Scripts {
|
|||
* @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
|
||||
* @param boolean $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
|
||||
*/
|
||||
private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) {
|
||||
private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = array( 'strategy' => 'defer' ) ) {
|
||||
if ( ! in_array( $handle, self::$scripts, true ) && $path ) {
|
||||
self::register_script( $handle, $path, $deps, $version, $in_footer );
|
||||
}
|
||||
|
|
|
@ -448,7 +448,6 @@ function wc_scheduled_sales() {
|
|||
|
||||
if ( $sale_price ) {
|
||||
$product->set_price( $sale_price );
|
||||
$product->set_date_on_sale_from( '' );
|
||||
} else {
|
||||
$product->set_date_on_sale_to( '' );
|
||||
$product->set_date_on_sale_from( '' );
|
||||
|
|
|
@ -2338,7 +2338,7 @@ if ( ! function_exists( 'woocommerce_breadcrumb' ) ) {
|
|||
'woocommerce_breadcrumb_defaults',
|
||||
array(
|
||||
'delimiter' => ' / ',
|
||||
'wrap_before' => '<nav class="woocommerce-breadcrumb">',
|
||||
'wrap_before' => '<nav class="woocommerce-breadcrumb" aria-label="Breadcrumb">',
|
||||
'wrap_after' => '</nav>',
|
||||
'before' => '',
|
||||
'after' => '',
|
||||
|
|
|
@ -137,7 +137,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
|||
'attributes' => [
|
||||
'property' => 'description',
|
||||
'label' => __( 'Note <optional />', 'woocommerce' ),
|
||||
'helpText' => '',
|
||||
'helpText' => 'Enter an optional note displayed on the product page when customers select this variation.',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
@ -475,11 +475,30 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
|
|||
],
|
||||
]
|
||||
);
|
||||
// Virtual section.
|
||||
$shipping_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-virtual-section',
|
||||
'order' => 20,
|
||||
]
|
||||
)->add_block(
|
||||
[
|
||||
'id' => 'product-variation-virtual',
|
||||
'blockName' => 'woocommerce/product-toggle-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'property' => 'virtual',
|
||||
'checkedValue' => false,
|
||||
'uncheckedValue' => true,
|
||||
'label' => __( 'This variation requires shipping or pickup', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
// Product Shipping Section.
|
||||
$product_fee_and_dimensions_section = $shipping_group->add_section(
|
||||
[
|
||||
'id' => 'product-variation-fee-and-dimensions-section',
|
||||
'order' => 20,
|
||||
'order' => 30,
|
||||
'attributes' => [
|
||||
'title' => __( 'Fees & dimensions', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
|
|
|
@ -152,7 +152,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
|||
'blockName' => 'woocommerce/product-summary-field',
|
||||
'order' => 20,
|
||||
'attributes' => [
|
||||
'property' => 'description',
|
||||
'property' => 'short_description',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
@ -776,8 +776,10 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
|||
'blockName' => 'woocommerce/product-toggle-field',
|
||||
'order' => 10,
|
||||
'attributes' => [
|
||||
'property' => 'virtual',
|
||||
'label' => __( 'This product requires shipping or pickup', 'woocommerce' ),
|
||||
'property' => 'virtual',
|
||||
'checkedValue' => false,
|
||||
'uncheckedValue' => true,
|
||||
'label' => __( 'This product requires shipping or pickup', 'woocommerce' ),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
const { test, expect } = require( '@playwright/test' );
|
||||
const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
|
||||
|
||||
test.describe( 'Bulk edit orders', () => {
|
||||
test.use( { storageState: process.env.ADMINSTATE } );
|
||||
|
||||
let orderId1, orderId2, orderId3, orderId4, orderId5;
|
||||
|
||||
test.beforeAll( async ( { baseURL } ) => {
|
||||
const api = new wcApi( {
|
||||
url: baseURL,
|
||||
consumerKey: process.env.CONSUMER_KEY,
|
||||
consumerSecret: process.env.CONSUMER_SECRET,
|
||||
version: 'wc/v3',
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
orderId1 = response.data.id;
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
orderId2 = response.data.id;
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
orderId3 = response.data.id;
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
orderId4 = response.data.id;
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
orderId5 = response.data.id;
|
||||
} );
|
||||
} );
|
||||
|
||||
test.afterAll( async ( { baseURL } ) => {
|
||||
const api = new wcApi( {
|
||||
url: baseURL,
|
||||
consumerKey: process.env.CONSUMER_KEY,
|
||||
consumerSecret: process.env.CONSUMER_SECRET,
|
||||
version: 'wc/v3',
|
||||
} );
|
||||
await api.delete( `orders/${ orderId1 }`, { force: true } );
|
||||
await api.delete( `orders/${ orderId2 }`, { force: true } );
|
||||
await api.delete( `orders/${ orderId3 }`, { force: true } );
|
||||
await api.delete( `orders/${ orderId4 }`, { force: true } );
|
||||
await api.delete( `orders/${ orderId5 }`, { force: true } );
|
||||
} );
|
||||
|
||||
test( 'can bulk update order status', async ( { page } ) => {
|
||||
await page.goto( 'wp-admin/admin.php?page=wc-orders' );
|
||||
|
||||
// expect order status 'processing' to show
|
||||
await expect( page.locator( `#order-${ orderId1 }`).getByText( 'Processing') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId2 }`).getByText( 'Processing') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId3 }`).getByText( 'Processing') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId4 }`).getByText( 'Processing') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId5 }`).getByText( 'Processing') ).toBeVisible();
|
||||
|
||||
await page.locator( '#cb-select-all-1' ).click();
|
||||
await page.locator( '#bulk-action-selector-top' ).selectOption( 'Change status to completed' );
|
||||
await page.locator('#doaction').click();
|
||||
|
||||
// expect order status 'completed' to show
|
||||
await expect( page.locator( `#order-${ orderId1 }`).getByText( 'Completed') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId2 }`).getByText( 'Completed') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId3 }`).getByText( 'Completed') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId4 }`).getByText( 'Completed') ).toBeVisible();
|
||||
await expect( page.locator( `#order-${ orderId5 }`).getByText( 'Completed') ).toBeVisible();
|
||||
} );
|
||||
|
||||
} );
|
|
@ -5,7 +5,7 @@ const uuid = require( 'uuid' );
|
|||
test.describe( 'Edit order', () => {
|
||||
test.use( { storageState: process.env.ADMINSTATE } );
|
||||
|
||||
let orderId;
|
||||
let orderId, orderToCancel;
|
||||
|
||||
test.beforeAll( async ( { baseURL } ) => {
|
||||
const api = new wcApi( {
|
||||
|
@ -21,6 +21,13 @@ test.describe( 'Edit order', () => {
|
|||
.then( ( response ) => {
|
||||
orderId = response.data.id;
|
||||
} );
|
||||
await api
|
||||
.post( 'orders', {
|
||||
status: 'processing',
|
||||
} )
|
||||
.then( ( response ) => {
|
||||
orderToCancel = response.data.id;
|
||||
} );
|
||||
} );
|
||||
|
||||
test.afterAll( async ( { baseURL } ) => {
|
||||
|
@ -31,6 +38,7 @@ test.describe( 'Edit order', () => {
|
|||
version: 'wc/v3',
|
||||
} );
|
||||
await api.delete( `orders/${ orderId }`, { force: true } );
|
||||
await api.delete( `orders/${ orderToCancel }`, { force: true } );
|
||||
} );
|
||||
|
||||
test( 'can view single order', async ( { page } ) => {
|
||||
|
@ -66,6 +74,31 @@ test.describe( 'Edit order', () => {
|
|||
await expect(
|
||||
page.locator( '#woocommerce-order-notes .note_content >> nth=0' )
|
||||
).toContainText( 'Order status changed from Processing to Completed.' );
|
||||
|
||||
// load the orders listing and confirm order is completed
|
||||
await page.goto( 'wp-admin/admin.php?page=wc-orders' );
|
||||
|
||||
await expect( page.locator( `#order-${ orderId }` ).getByRole( 'cell', { name: 'Completed' }) ).toBeVisible();
|
||||
} );
|
||||
|
||||
test( 'can update order status to cancelled', async ( { page } ) => {
|
||||
// open order we created
|
||||
await page.goto( `wp-admin/post.php?post=${ orderToCancel }&action=edit` );
|
||||
|
||||
// update order status to Completed
|
||||
await page.locator( '#order_status' ).selectOption( 'Cancelled' );
|
||||
await page.locator( 'button.save_order' ).click();
|
||||
|
||||
// verify order status changed and note added
|
||||
await expect( page.locator( '#order_status' ) ).toHaveValue(
|
||||
'wc-cancelled'
|
||||
);
|
||||
await expect( page.getByText( 'Order status changed from Processing to Cancelled.' ) ).toBeVisible();
|
||||
|
||||
// load the orders listing and confirm order is cancelled
|
||||
await page.goto( 'wp-admin/admin.php?page=wc-orders' );
|
||||
|
||||
await expect( page.locator( `#order-${ orderToCancel }` ).getByRole( 'cell', { name: 'Cancelled' }) ).toBeVisible();
|
||||
} );
|
||||
|
||||
test( 'can update order details', async ( { page } ) => {
|
||||
|
@ -87,6 +120,40 @@ test.describe( 'Edit order', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
test( 'can add and delete order notes', async ( { page } ) => {
|
||||
// open order we created
|
||||
await page.goto( `wp-admin/post.php?post=${ orderId }&action=edit` );
|
||||
await page.on( 'dialog', dialog => dialog.accept() );
|
||||
|
||||
// add an order note
|
||||
await page.getByLabel( 'Add note' ).fill( 'This order is a test order. It is only a test. This note is a private note.' );
|
||||
await page.getByRole( 'button', { name: 'Add', exact: true } ).click();
|
||||
|
||||
// verify the note saved
|
||||
await expect( page.getByText( 'This order is a test order. It is only a test. This note is a private note.' ) ).toBeVisible();
|
||||
|
||||
// delete the note
|
||||
await page.getByRole( 'button', { name: 'Delete note' } ).first().click();
|
||||
|
||||
// verify the note is gone
|
||||
await expect( page.getByText( 'This order is a test order. It is only a test. This note is a private note.' ) ).not.toBeVisible();
|
||||
|
||||
// add note to customer
|
||||
// add an order note
|
||||
await page.getByLabel( 'Add note' ).fill( 'This order is a test order. It is only a test. This note is a note to the customer.' );
|
||||
await page.getByLabel('Note type').selectOption( 'Note to customer' );
|
||||
await page.getByRole( 'button', { name: 'Add', exact: true } ).click();
|
||||
|
||||
// verify the note saved
|
||||
await expect( page.getByText( 'This order is a test order. It is only a test. This note is a note to the customer.' ) ).toBeVisible();
|
||||
|
||||
// delete the note
|
||||
await page.getByRole( 'button', { name: 'Delete note' } ).first().click();
|
||||
|
||||
// verify the note is gone
|
||||
await expect( page.getByText( 'This order is a test order. It is only a test. This note is a private note.' ) ).not.toBeVisible();
|
||||
} );
|
||||
|
||||
test( 'can load billing details', async ( { page, baseURL } ) => {
|
||||
let customerId = 0;
|
||||
|
||||
|
|
1717
pnpm-lock.yaml
1717
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue