Merge branch 'trunk' into feature/marketplace-subscriptions

This commit is contained in:
berislav grgičak 2023-10-18 12:11:16 +02:00 committed by GitHub
commit 83517af699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
338 changed files with 7735 additions and 1610 deletions

View File

@ -0,0 +1,19 @@
module.exports = async ( { github, context, core } ) => {
const { ASSET_ID: asset_id } = process.env;
const { owner, repo } = context.repo;
const fs = require( 'fs' );
const path = require( 'path' );
const response = await github.rest.repos.getReleaseAsset( {
owner,
repo,
asset_id,
headers: { accept: 'application/octet-stream' },
} );
const zipPath = path.resolve( 'tmp', 'woocommerce.zip' );
fs.mkdirSync( 'tmp' );
fs.writeFileSync( zipPath, Buffer.from( response.data ) );
core.setOutput( 'zip-path', zipPath );
};

View File

@ -0,0 +1,51 @@
module.exports = async ( { github, context, core } ) => {
const { RELEASE_VERSION, GITHUB_EVENT_NAME } = process.env;
async function findRelease() {
const { owner, repo } = context.repo;
const list = await github.rest.repos.listReleases( {
owner,
repo,
per_page: 100,
} );
const match = list.data.find( ( { tag_name, name } ) =>
[ tag_name, name ].includes( RELEASE_VERSION )
);
return match;
}
async function handleWorkflowDispatch() {
const match = await findRelease();
if ( match ) {
return match;
}
throw new Error(
`"${ RELEASE_VERSION }" is not a valid release version!`
);
}
function findWooCommerceZipAsset() {
const match = release.assets.find(
( { name } ) => name === 'woocommerce.zip'
);
if ( ! match ) {
throw new Error(
`Release ${ RELEASE_VERSION } does not contain a woocommerce.zip asset!`
);
}
return match;
}
const release =
GITHUB_EVENT_NAME === 'release'
? await findRelease()
: await handleWorkflowDispatch();
const asset = findWooCommerceZipAsset();
core.setOutput( 'version', RELEASE_VERSION );
core.setOutput( 'created', release.created_at );
core.setOutput( 'asset-id', asset.id );
};

View File

@ -5,7 +5,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'WooCommerce Release Tag'
description: 'WooCommerce release version'
required: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.release.tag_name || inputs.tag }}
@ -16,55 +16,33 @@ env:
E2E_UPDATE_WC_ARTIFACT: WooCommerce version update test on release smoke test site (run ${{ github.run_number }})
SLACK_BLOCKS_ARTIFACT: slack-blocks
jobs:
get-tag:
name: Get WooCommerce release tag
validate-version:
name: Validate release version
permissions:
contents: read
runs-on: ubuntu-20.04
outputs:
tag: ${{ steps.get-tag.outputs.tag }}
created: ${{ steps.created-at.outputs.created }}
version: ${{ steps.validate-version.outputs.version }}
created: ${{ steps.validate-version.outputs.created }}
asset-id: ${{ steps.validate-version.outputs.asset-id }}
steps:
- name: Validate tag
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release view "${{ inputs.tag }}" --repo=woocommerce/woocommerce
- uses: actions/checkout@v3
- name: Get tag from triggered event
id: get-tag
- name: Validate release version
id: validate-version
uses: actions/github-script@v6
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
run: |
echo "Triggered event: ${{ github.event_name }}"
echo "Tag from event: $RELEASE_TAG"
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
- name: Verify woocommerce.zip asset
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
RELEASE_TAG: ${{ steps.get-tag.outputs.tag }}
run: |
ASSET_NAMES=$(gh release view $RELEASE_TAG --repo woocommerce/woocommerce --json assets --jq ".assets[].name")
if [[ $ASSET_NAMES == *"woocommerce.zip"* ]]
then
echo "$RELEASE_TAG has a valid woocommerce.zip asset."
exit 0
fi
echo "$RELEASE_TAG does not have a valid woocommerce.zip asset."
exit 1
- name: Get 'created-at' of WooCommerce zip
id: created-at
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: echo "created=$(gh release view ${{ steps.get-tag.outputs.tag }} --json assets --jq .assets[0].createdAt --repo woocommerce/woocommerce)" >> $GITHUB_OUTPUT
RELEASE_VERSION: ${{ inputs.tag }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/validate-release-version.js');
await script({ github, context, core });
e2e-update-wc:
name: Test WooCommerce update
runs-on: ubuntu-20.04
needs: [get-tag]
needs: [validate-version]
permissions:
contents: read
env:
@ -94,7 +72,7 @@ jobs:
CUSTOMER_USER: ${{ secrets.RELEASE_TEST_CUSTOMER_USER }}
DEFAULT_TIMEOUT_OVERRIDE: 120000
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
UPDATE_WC: ${{ needs.get-tag.outputs.tag }}
UPDATE_WC: ${{ needs.validate-version.outputs.version }}
- name: Upload Allure artifacts to bucket
if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' )
@ -113,10 +91,10 @@ jobs:
ENV_DESCRIPTION: wp-latest
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_WP_LATEST_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -132,12 +110,12 @@ jobs:
test-name: WC Update test
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: wp-latest
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
api-wp-latest:
name: API on WP Latest
runs-on: ubuntu-20.04
needs: [get-tag, e2e-update-wc]
needs: [validate-version, e2e-update-wc]
permissions:
contents: read
outputs:
@ -170,7 +148,8 @@ jobs:
API_BASE_URL: ${{ secrets.RELEASE_TEST_URL }}
USER_KEY: ${{ secrets.RELEASE_TEST_ADMIN_USER }}
USER_SECRET: ${{ secrets.RELEASE_TEST_ADMIN_PASSWORD }}
UPDATE_WC: ${{ needs.get-tag.outputs.tag }}
UPDATE_WC: ${{ needs.validate-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
- name: Upload Allure artifacts to bucket
if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' )
@ -189,10 +168,10 @@ jobs:
ENV_DESCRIPTION: wp-latest
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.API_WP_LATEST_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="api" \
@ -208,12 +187,12 @@ jobs:
test-name: WP Latest
api-result: ${{ steps.run-api-composite-action.outputs.result }}
env-slug: wp-latest
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
e2e-wp-latest:
name: E2E on WP Latest
runs-on: ubuntu-20.04
needs: [get-tag, api-wp-latest]
needs: [validate-version, api-wp-latest]
permissions:
contents: read
env:
@ -291,10 +270,10 @@ jobs:
ENV_DESCRIPTION: wp-latest
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_WP_LATEST_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -311,12 +290,12 @@ jobs:
api-result: ${{ needs.api-wp-latest.outputs.result }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: wp-latest
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
test-wp-latest-1:
name: Test against WP Latest-1
runs-on: ubuntu-20.04
needs: [ get-tag ]
needs: [validate-version]
env:
API_ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-report
API_ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/api/allure-results
@ -350,12 +329,20 @@ jobs:
WP_ENV_CORE: WordPress/WordPress#${{ steps.get-wp-latest-1.outputs.version }}
- name: Download release zip
id: download-zip
uses: actions/github-script@v6
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release download ${{ steps.get-wp-latest-1.outputs.version }} --dir tmp
ASSET_ID: ${{ needs.validate-version.outputs.asset-id }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/download-release-zip.js');
await script({ github, context, core });
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
run: unzip -d plugins -o tmp/woocommerce.zip
run: unzip -d plugins -o ${{ env.ZIP_PATH }}
env:
ZIP_PATH: ${{ steps.download-zip.outputs.zip-path }}
- name: Run API tests
id: run-api-composite-action
@ -387,10 +374,10 @@ jobs:
ENV_DESCRIPTION: wp-latest-1
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.API_WP_LATEST_X_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="api" \
@ -428,10 +415,10 @@ jobs:
ENV_DESCRIPTION: wp-latest-1
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_WP_LATEST_X_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -451,12 +438,12 @@ jobs:
api-result: ${{ steps.run-api-composite-action.outputs.result }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: wp-latest-1
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
test-php-versions:
name: Test against PHP ${{ matrix.php_version }}
runs-on: ubuntu-20.04
needs: [get-tag]
needs: [validate-version]
strategy:
fail-fast: false
matrix:
@ -490,12 +477,20 @@ jobs:
run: bash verify-php-version.sh
- name: Download release zip
id: download-zip
uses: actions/github-script@v6
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release download ${{ needs.get-tag.outputs.tag }} --dir tmp
ASSET_ID: ${{ needs.validate-version.outputs.asset-id }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/download-release-zip.js');
await script({ github, context, core });
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
run: unzip -d plugins -o tmp/woocommerce.zip
run: unzip -d plugins -o ${{ env.ZIP_PATH }}
env:
ZIP_PATH: ${{ steps.download-zip.outputs.zip-path }}
- name: Run API tests
id: run-api-composite-action
@ -527,10 +522,10 @@ jobs:
ENV_DESCRIPTION: php-${{ matrix.php_version }}
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.API_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="api" \
@ -568,10 +563,10 @@ jobs:
ENV_DESCRIPTION: php-${{ matrix.php_version }}
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.E2E_ARTIFACT }}" \
-f env_description="${{ env.ENV_DESCRIPTION }}" \
-f test_type="e2e" \
@ -591,12 +586,12 @@ jobs:
api-result: ${{ steps.run-api-composite-action.outputs.result }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: php-${{ matrix.php_version }}
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
test-plugins:
name: With ${{ matrix.plugin }}
runs-on: ubuntu-20.04
needs: [get-tag]
needs: [validate-version]
env:
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
@ -638,12 +633,20 @@ jobs:
run: pnpm run env:test
- name: Download release zip
id: download-zip
uses: actions/github-script@v6
env:
GH_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
run: gh release download ${{ needs.get-tag.outputs.tag }} --dir tmp
ASSET_ID: ${{ needs.validate-version.outputs.asset-id }}
with:
github-token: ${{ secrets.E2E_GH_TOKEN }}
script: |
const script = require('./.github/workflows/scripts/download-release-zip.js');
await script({ github, context, core });
- name: Replace `plugins/woocommerce` with unzipped woocommerce release build
run: unzip -d plugins -o tmp/woocommerce.zip
run: unzip -d plugins -o ${{ env.ZIP_PATH }}
env:
ZIP_PATH: ${{ steps.download-zip.outputs.zip-path }}
- name: Run 'Upload plugin' test
id: run-upload-test
@ -686,10 +689,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
run: |
gh workflow run publish-test-reports-release.yml \
-f created_at="${{ needs.get-tag.outputs.created }}" \
-f created_at="${{ needs.validate-version.outputs.created }}" \
-f run_id=${{ github.run_id }} \
-f run_number=${{ github.run_number }} \
-f release_tag=${{ needs.get-tag.outputs.tag }} \
-f release_tag=${{ needs.validate-version.outputs.version }} \
-f artifact="${{ env.ARTIFACT_NAME }}" \
-f env_description="${{ matrix.env_description }}" \
-f test_type="e2e" \
@ -704,7 +707,7 @@ jobs:
test-name: With ${{ matrix.plugin }}
e2e-result: ${{ steps.run-e2e-composite-action.outputs.result }}
env-slug: ${{ matrix.env_description }}
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
post-slack-summary:
name: Post Slack summary
@ -717,7 +720,7 @@ jobs:
)
needs:
- e2e-wp-latest
- get-tag
- validate-version
- test-php-versions
- test-plugins
- test-wp-latest-1
@ -735,7 +738,7 @@ jobs:
id: run-payload-action
uses: ./.github/actions/tests/slack-summary-on-release/slack-payload
with:
release-version: ${{ needs.get-tag.outputs.tag }}
release-version: ${{ needs.validate-version.outputs.version }}
blocks-dir: ${{ steps.download-slack-blocks.outputs.download-path }}
- name: Send Slack message

View File

@ -1,11 +1,8 @@
name: Add Triage Label
on:
issues:
types: opened
permissions: {}
jobs:
add_label:
runs-on: ubuntu-20.04
@ -14,7 +11,22 @@ jobs:
issues: write
steps:
- uses: actions/checkout@v3
# We want to delay the labeling of the issue so that the author has a change to add labels after issue creation.
- name: 'Delay Labeling'
run: sleep 3m
# Make sure that the latest issue is pulled from the database rather than relying on the payload. This is
# because the payload won't include any labels that were added after the issue was created.
- uses: actions/github-script@v6
id: latest-issue
with:
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
core.setOutput('hasLabels', issue.data.labels.length > 0);
- uses: actions-ecosystem/action-add-labels@v1
if: github.event.issue.labels[0] == null
if: ${{ steps.latest-issue.outputs.hasLabels == 'false'}}
with:
labels: 'status: awaiting triage'

View File

@ -0,0 +1,131 @@
#!/usr/bin/env node
const https = require( 'https' );
const vm = require( 'vm' );
const fs = require( 'fs' );
const path = require( 'path' );
const intlUrl =
'https://raw.githubusercontent.com/jackocnr/intl-tel-input/master/src/js/data.js';
const phoneUrl =
'https://raw.githubusercontent.com/AfterShip/phone/master/src/data/country_phone_data.ts';
const fetch = ( url ) =>
new Promise( ( resolve, reject ) => {
https
.get( url, ( res ) => {
let body = '';
res.on( 'data', ( chunk ) => {
body += chunk;
} );
res.on( 'end', () => {
resolve( body );
} );
} )
.on( 'error', reject );
} );
const numberOrString = ( str ) =>
Number( str ).toString().length !== str.length ? str : Number( str );
const evaluate = ( code ) => {
const script = new vm.Script( code );
const context = vm.createContext();
script.runInContext( context );
return context;
};
const parse = ( data /*: any[]*/ ) /*: DataType*/ =>
data.reduce(
( acc, item ) => ( {
...acc,
[ item[ 0 ] ]: {
alpha2: item[ 0 ],
code: item[ 1 ].toString(),
priority: item[ 2 ] || 0,
start: item[ 3 ]?.map( String ),
lengths: item[ 4 ],
},
} ),
{}
);
const saveToFile = ( data ) => {
const dataString = JSON.stringify( data ).replace( /null/g, '' );
const parseString = parse.toString().replace( / \/\*(.+?)\*\//g, '$1' );
const code = [
'// Do not edit this file directly.',
'// Generated by /bin/packages/js/components/phone-number-input/build-data.js',
'',
'/* eslint-disable */',
'',
'import type { DataType } from "./types";',
'',
`const parse = ${ parseString }`,
'',
`const data = ${ dataString }`,
'',
'export default parse(data);',
].join( '\n' );
const filePath = path.resolve(
'packages/js/components/src/phone-number-input/data.ts'
);
fs.writeFileSync( filePath, code );
};
( async () => {
const intlData = await fetch( intlUrl ).then( evaluate );
const phoneData = await fetch( phoneUrl )
.then( ( data ) => 'var data = ' + data.substring( 15 ) )
.then( evaluate );
// Convert phoneData array to object
const phoneCountries = phoneData.data.reduce(
( acc, item ) => ( {
...acc,
[ item.alpha2.toLowerCase() ]: item,
} ),
{}
);
// Traverse intlData to create a new array with required fields
const countries = intlData.allCountries.map( ( item ) => {
const phoneCountry = phoneCountries[ item.iso2 ];
const result = [
item.iso2.toUpperCase(), // alpha2
Number( item.dialCode ), // code
/* [2] priority */
/* [3] start */
/* [4] lengths */
,
,
,
];
if ( item.priority ) {
result[ 2 ] = item.priority;
}
const areaCodes = item.areaCodes || [];
const beginWith = phoneCountry?.mobile_begin_with || [];
if ( areaCodes.length || beginWith.length ) {
result[ 3 ] = [ ...new Set( [ ...areaCodes, ...beginWith ] ) ].map(
numberOrString
);
}
if ( phoneCountry?.phone_number_lengths ) {
result[ 4 ] = phoneCountry.phone_number_lengths;
}
return result;
} );
saveToFile( countries );
} )();

View File

@ -1,5 +1,12 @@
== Changelog ==
= 8.2.1 2023-10-16 =
**WooCommerce**
* Fix - Prevent global attribute terms from being automatically selected [#40729](https://github.com/woocommerce/woocommerce/pull/40729)
= 8.2.0 2023-10-13 =
**WooCommerce**

View File

@ -1,7 +1,5 @@
# Adjust the quantity input values
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our  [Support Policy](http://www.woocommerce.com/support-policy/).
Set the starting value, maximum value, minimum value, and increment amount for quantity input fields on product pages.
Add this code to your child themes `functions.php` file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file, as this will be wiped entirely when you update the theme.
@ -34,7 +32,7 @@ if ( ! function_exists( 'YOUR_PREFIX_woocommerce_available_variation' ) ) {
function YOUR_PREFIX_woocommerce_available_variation( $args ) {
$args['max_qty'] = 20; // Maximum value (variations)
$args['min_qty'] = 2; // Minimum value (variations)
// Note: the starting value and step for variations is controlled
// from the 'woocommerce_quantity_input_args' filter shown above for
// simple products

View File

@ -1,9 +1,9 @@
# Add a message above the login / register form
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
This code will add a custom message above the login/register form on the users my-account page.
Add this code to your child themes `functions.php` file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file, as this will be wiped entirely when you update the theme.
```php
if ( ! function_exists( 'YOUR_PREFIX_login_message' ) ) {
/**
@ -30,5 +30,5 @@ if ( ! function_exists( 'YOUR_PREFIX_login_message' ) ) {
Please note that for this code to work, the following options must be checked in the WooCommerce “Accounts & Privacy” settings:
- Allow customers to create an account during checkout.
- Allow customers to create an account on the "My Account" page.
- Allow customers to create an account during checkout.
- Allow customers to create an account on the "My Account" page.

View File

@ -1,7 +1,5 @@
# Change number of related products output
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/?).
Add code to your child themes functions.php file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file as this will be wiped entirely when you update the theme.
Please note that it does not work for all themes because of the way theyre coded.

View File

@ -1,7 +1,5 @@
# Unhook and remove WooCommerce emails
> This is a **Developer level** doc. If you are unfamiliar with code and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our  [Support Policy](http://www.woocommerce.com/support-policy/).
This code allows you to unhook and remove the default WooCommerce emails.
Add this code to your child themes `functions.php` file or via a plugin that allows custom functions to be added, such as the [Code snippets](https://wordpress.org/plugins/code-snippets/) plugin. Avoid adding custom code directly to your parent themes `functions.php` file, as this will be wiped entirely when you update the theme.

View File

@ -0,0 +1,138 @@
# Adding a Section to a Settings Tab
When youre adding building an extension for WooCommerce that requires settings of some kind, its important to ask yourself: **Where do they belong?** If your extension just has a couple of simple settings, do you really need to create a new tab specifically for it? Most likely the answer is no.
## [When to Create a Section](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-1)
Lets say we had an extension that adds a slider to a single product page. This extension doesnt have many options, just a couple:
- Auto-insert into single product page (checkbox)
- Slider Title (text field)
Thats only two options, specifically related to **Products**. We could quite easily just append them onto the core WooCommerce Products Settings (**WooCommerce > Settings > Products**), but that wouldnt be very user friendly. Users wouldnt know where to look initially so theyd have to scan all of the Products options and it would be difficult / impossible to link the options directly. Fortunately, as of WooCommerce 2.2.2, there is a new filter in place that allows you add a new **section**, beneath one of the core settings tabs.
> **Note:** This is a **Developer level** doc. If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
## [How to Create a Section](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-2)
Well go over doing this through individual functions, but you should probably create a Class that stores all of your settings methods.
The first thing you need to is add the section, which can be done like this by hooking into the `woocommerce_get_sections_products` filter:
```php
/**
* Create the section beneath the products tab
**/
add_filter( 'woocommerce_get_sections_products', 'wcslider_add_section' );
function wcslider_add_section( $sections ) {
$sections['wcslider'] = __( 'WC Slider', 'text-domain' );
return $sections;
}
```
_[wc-create-section-beneath-products.php](https://gist.github.com/woogists/2964ec01c8bea50fcce62adf2f5c1232/raw/da5348343cf3664c0bc8b6b132d8105bfcf9ca51/wc-create-section-beneath-products.php)_
Make sure you change the **wcslider** parts to suit your extensions name / text-domain. The important thing about the `woocommerce_get_sections_products` filter, is that the last part **products**, is the tab youd like to add a section to. So if you want to add a new tab to accounts section, you would hook into the `woocommerce_get_sections_accounts` filter.
## [How to Add Settings to a Section](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-3)
Now that youve got the tab, you need to filter the output of `woocommerce_get_sections_products` (or similar). You would add the settings like usual using the [**WooCommerce Settings API**](https://github.com/woocommerce/woocommerce/blob/trunk/docs/settings-api/), but check for the current section before adding the settings to the tabs settings array. For example, lets add the sample settings we discussed above to the new **wcslider** section we just created:
```php
/**
* Add settings to the specific section we created before
*/
add_filter( 'woocommerce_get_settings_products', 'wcslider_all_settings', 10, 2 );
function wcslider_all_settings( $settings, $current_section ) {
/**
* Check the current section is what we want
**/
if ( $current_section == 'wcslider' ) {
$settings_slider = array();
// Add Title to the Settings
$settings_slider[] = array( 'name' => __( 'WC Slider Settings', 'text-domain' ), 'type' => 'title', 'desc' => __( 'The following options are used to configure WC Slider', 'text-domain' ), 'id' => 'wcslider' );
// Add first checkbox option
$settings_slider[] = array(
'name' => __( 'Auto-insert into single product page', 'text-domain' ),
'desc_tip' => __( 'This will automatically insert your slider into the single product page', 'text-domain' ),
'id' => 'wcslider_auto_insert',
'type' => 'checkbox',
'css' => 'min-width:300px;',
'desc' => __( 'Enable Auto-Insert', 'text-domain' ),
);
// Add second text field option
$settings_slider[] = array(
'name' => __( 'Slider Title', 'text-domain' ),
'desc_tip' => __( 'This will add a title to your slider', 'text-domain' ),
'id' => 'wcslider_title',
'type' => 'text',
'desc' => __( 'Any title you want can be added to your slider with this option!', 'text-domain' ),
);
$settings_slider[] = array( 'type' => 'sectionend', 'id' => 'wcslider' );
return $settings_slider;
/**
* If not, return the standard settings
**/
} else {
return $settings;
}
}
```
_[wc-add-settings-section.php](https://gist.github.com/woogists/4038b83900508806c57a193a2534b845#file-wc-add-settings-section-php)_
Were hooking into the same `woocommerce_get_sections_products` filter, but this time doing a check that the `$current_section` matches our earlier defined custom section (wcslider), before adding in our new settings.
## [Using the New Settings](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-4)
You would now just use your newly created settings like you would any other WordPress / WooCommerce setting, through the [**get_option**](http://codex.wordpress.org/Function_Reference/get_option) function and the defined ID of the setting. For example, to use the previously created **wcslider_auto_insert** option, simply use the following code: `get_option( 'wcslider_auto_insert' )`
## [Conclusion](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/adding-a-section-to-a-settings-tab.md#section-5)
When creating an extension for WooCommerce, think about where your settings belong before you create them. The key to building a useful product is making it easy to use for the end user, so appropriate setting placement is crucially important. For more specific information on adding settings to WooCommerce, check out the [**Settings API documentation**](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extesion-developlment/settings-api/).

View File

@ -0,0 +1,54 @@
# Classes in WooCommerce
## [List of Classes in WooCommerce](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-1)
For a list of Classes in WooCommerce, please see the [WooCommerce Code Reference](https://woocommerce.github.io/code-reference/packages/WooCommerce-Classes.html).
## [Common Classes](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-2)
### [WooCommerce](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-3)
The main class is `woocommerce` which is available globally via the `$woocommerce` variable. This handles the main functions of WooCommerce and inits other classes, stores site-wide variables, and handles error/success messages. The woocommerce class initializes the following classes when constructed:
- `WC_Query` stored in `$woocommerce->query`
- `WC_Customer` stored in `$woocommerce->customer`
- `WC_Shipping` stored in `$woocommerce->shipping`
- `WC_Payment_Gateways` stored in `$woocommerce->payment_gateways`
- `WC_Countries` stored in `$woocommerce->countries`
Other classes are auto-loaded on demand.
View the [WooCommerce Class Code Reference](https://woocommerce.github.io/code-reference/classes/WooCommerce.html) for a full list of methods contained in this class.
### [WC_Product](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-4)
WooCommerce has several product classes responsible for loading and outputting product data. This can be loaded through PHP using:
`$product = wc_get_product( $post->ID );`
In the loop this is not always necessary since calling `the_post()` will automatically populate the global `$product` variable if the post is a product.
View the [WC_Product Code Reference](https://woocommerce.github.io/code-reference/classes/WC-Product.html) for a full list of methods contained in this class.
### [WC_Customer](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-5)
The customer class allows you to get data about the current customer, for example:
```php
global $woocommerce;
$customer_country = $woocommerce->customer->get_country();
```
View the [WC_Customer Code Reference](https://woocommerce.github.io/code-reference/classes/WC-Customer.html) for a full list of methods contained in this class.
### [WC_Cart](https://github.com/woocommerce/woocommerce/blob/trunk/docs/extension-development/class-reference#section-6)
The cart class loads and stores the users cart data in a session. For example, to get the cart subtotal you could use:
```php
global $woocommerce;
$cart_subtotal = $woocommerce->cart->get_cart_subtotal();
```
View the [WC_Cart Code Reference](https://woocommerce.github.io/code-reference/classes/WC-Cart.html) for a full list of methods contained in this class.

View File

@ -0,0 +1,38 @@
# Using custom attributes in menus and taxonomy archives
Attributes that can be used for the layered nav are a custom taxonomy, which means you can display them in menus, or display products by attributes. This requires some work on your part, and archives must be enabled.
> **Note:** This is a **Developer level** doc. If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
# Register the taxonomy for menus
When registering taxonomies for your custom attributes, WooCommerce calls the following hook:
```php
$show_in_nav_menus = apply_filters('woocommerce_attribute_show_in_nav_menus', false, $name);
```
So, for example, if your attribute slug was `size` you would do the following to register it for menus:
```php
add_filter('woocommerce_attribute_show_in_nav_menus', 'wc_reg_for_menus', 1, 2);
function wc_reg_for_menus( $register, $name = '' ) {
if ( $name == 'pa_size' ) $register = true;
return $register;
}
```
Custom attribute slugs are prefixed with `pa_`, so an attribute called `size` would be `pa_size`
Now use your attribute in **Appearance > Menus**. You will notice, however, that it has default blog styling when you click on a link to your taxonomy term.
# Create a template
You need to theme your attribute to make it display products as you want. To do this:
1. Copy `woocommerce/templates/taxonomy-product_cat.php` into your theme folder
2. Rename the template to reflect your attribute in our example wed use `taxonomy-pa_size.php`
You should now see this template when viewing taxonomy terms for your custom attribute.

View File

@ -0,0 +1,162 @@
# High Performance Order Storage (HPOS)
WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues.
[High-Performance Order Storage (HPOS)](https://developer.woocommerce.com/2022/09/14/high-performance-order-storage-progress-report/) also previously known as “Custom Order Tables” is a solution that provides an easy-to-understand and solid database structure specifically designed for eCommerce needs. It uses the WooCommerce CRUD design to store order data in custom tables optimized for WooCommerce queries with minimal impact on the stores performance.
In January 2022, we published the [initial plan for the Custom Order Tables feature](https://developer.woocommerce.com/2022/01/17/the-plan-for-the-woocommerce-custom-order-table/) and since then, weve been working hard to bring the High-Performance Order Storage (HPOS) to WooCommerce Core. In May 2022, we invited you to [test the order migration process](https://developer.woocommerce.com/2022/05/16/call-for-early-testing-custom-order-table-migrations/) and provide feedback on how our initial work performs on real stores of varied configurations.
From WooCommerce 8.2, released on October 2023, [High-Performance Order Storage (HPOS)](https://developer.woocommerce.com/2022/09/14/high-performance-order-storage-progress-report/) is officially released under the stable flag and will be enabled by default for new installations.
## [Whats New with High-Performance Order Storage?](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-1)
Bringing High-Performance Order Storage (HPOS) to WooCommerce improves these three essential properties for eCommerce stores.
1/ **Scalability**
The rise in the number of customers and customer orders increases the load on your stores database making it difficult to handle customer order requests and deliver a seamless user experience.
With High-Performance Order Storage, you get dedicated tables for data like orders and order addresses and thus dedicated indexes which results in fewer read/write operations and fewer busy tables. This feature enables eCommerce stores of all shapes and sizes to scale their business to their maximum potential without expert intervention.
2/ **Reliability**
High-Performance Order Storage makes implementing and restoring targeted data backup easier. Youll no longer need to worry about losing orders, inventory numbers, or client information with reliable backup in these custom order tables. Itll also facilitate implementing read/write locks and prevent race conditions.
3/ **Simplicity**
You no longer have to go through a single huge database to locate underlying data and WooCommerce entries.
With High-Performance Order Storage, you can easily browse through the separate tables and easy-to-handle entries, independent of the table `_posts`, to find data or understand the table structure. It also lets you easily develop new plugins, implement designs for shops and products, and modify WooCommerce with more flexibility.
## [Background](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-2)
Before the release of version 8.2, WooCommerce relied on the `_post` and `_postmeta` table structures to store order information, which has served well over the years.
However, High-Performance Order Storage introduces dedicated tables for data like orders and order addresses and thus dedicated indexes which results in fewer read/write operations and fewer busy tables. This feature enables eCommerce stores of all shapes and sizes to scale their business to their maximum potential without expert intervention.
The order data is synced from `_posts` and `_postmeta` table to four custom order tables:
1. `_wc_orders`
2. `_wc_order_addresses`
3. `_wc_order_operational_data`
4. `_wc_orders_meta`
## [Enabling the feature](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-3)
From WooCommerce 8.2, released on October 2023, HPOS is enabled by default for new installations. Existing stores can check [How to enable HPOS](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/enable-hpos.md)
## [Database tables](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-4)
A number of database tables are used to store order data by HPOS. The `get_all_table_names` method in [the OrdersTableDataStore class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) will return the names of all the tables.
## [Authoritative tables](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-5)
At any given time, while the HPOS feature is enabled, there are two roles for the involved database tables: _authoritative_ and _backup_. The authoritative tables are the working tables, where order data will be stored to and retrieved from during normal operation of the store. The _backup_ tables will receive a copy of the authoritative data whenever [synchronization](#synchronization) happens.
If the `woocommerce_custom_orders_table_enabled` options is set to true, HPOS is active and [the new tables](#database-tables) are authoritative, while the posts and post meta tables act as the backup tables. If the option is set to false, it's the other way around. The option can be changed via admin UI (WooCommerce - Settings - Advanced - Custom data stores).
[The CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php) hooks on the `woocommerce_order_data_store` filter so that `WC_Data_Store::load( 'order' );` will return either an instance of [OrdersTableDataStore](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) or an instance of [WC_Order_Data_Store_CPT](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php), depending on which are the authoritative tables.
In order to preserve data integrity, switching the authoritative tables (from the new tables to the posts table or the other way around) isn't allowed while there are orders pending synchronization.
## [Synchronization](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-6)
_Synchronization_ is the process of applying all the pending changes in the authoritative tables to the backup tables. _Orders pending synchronization_ are orders that have been modified in the authoritative tables but the changes haven't been applied to the backup tables yet.
This can happen in a number of ways:
### Immediate synchronization
If the `woocommerce_custom_orders_table_data_sync_enabled` setting is set to true, synchronization happens automatically and immediately as soon as the orders are changed in the authoritative tables.
### Manual synchronization
When immediate synchronization is disabled, it can be triggered manually via command line as follows: `wp wc cot sync`. It can also be triggered programmatically as follows:
```php
$synchronizer = wc_get_container()->get(Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::class);
$order_ids = $synchronizer->get_next_batch_to_process( $batch_size );
if ( count( $order_ids ) ) {
$synchronizer->process_batch( $order_ids );
}
```
where `$batch_size` is the maximum count of orders to process.
### Scheduled synchronization
If immediate synchronization gets activated (`woocommerce_custom_orders_table_data_sync_enabled` is set to true) while there are orders pending synchronization, an instance of [DataSynchronizer](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php) will be enqueued using [BatchProcessingController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php) so that the synchronization of created/modified/deleted orders will happen in batches via scheduled actions. This scheduling happens inside [CustomOrdersTableController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php), by means of hooking into `woocommerce_update_options_advanced_custom_data_stores`.
If for some reason immediate synchronization is already active but synchronization is not scheduled, a trick to restart it is to go to the settings page (WooCommerce - Settings - Advanced - Custom data stores) and hit "Save" even without making any changes. As long as "Keep the posts table and the orders tables synchronized" is checked the synchronization scheduling will happen, even if it was checked before.
If the `woocommerce_auto_flip_authoritative_table_roles` option is set to true (there's a checkbox for it in the settings page), the authoritative tables will be switched automatically once all the orders have been synchronized. This is handled by [the CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php).
### Deletion synchronization
Synchronization of order deletions is tricky: if an order exists in one set of tables (new tables or posts) but not in the other, it's not clear if the missing orders need to be created or if the existing orders need to be deleted. Theoretically, the orders missing from the backup tables imply the former and the orders missing from the authoritative tables imply the latter; but that's dangerous as a bug in the involved code could easily lead to the deletion of legitimate orders.
To achieve a robust order deletion synchronization mechanism the following is done. Whenever an order is deleted and immediate synchronization is disabled, a record is created in the `wp_wc_orders_meta` table that has `deleted_from` as the key and the name of the authoritative table the order was deleted from (`wp_wc_orders` or the posts table). Then at synchronization time these records are processed (the corresponding orders are deleted from the corresponding tables) and deleted afterwards.
An exception to the above are the [placeholder records](#placeholder-records): these are deleted immediately when the corresponding order is deleted from `wp_wc_orders`, even if immediate synchronization is disabled.
When the “**High-Performance Order Storage**” and “**Compatibility mode**” are enabled, WooCommerce populates the HPOS tables with data from posts & postmeta tables. The synchronization between the tables is [explained in detail in this document](https://developer.woocommerce.com/2022/09/29/high-performance-order-storage-backward-compatibility-and-synchronization/#synchronization).
> You can find a deeper explanation about the synchronization between the tables in [this document about high-performance-order-storage-backward-compatibility-and-synchronization](https://developer.woocommerce.com/2022/09/29/high-performance-order-storage-backward-compatibility-and-synchronization/#synchronization).
## [Placeholder records](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-7)
Order IDs must match in both the authoritative tables and the backup tables, otherwise synchronization wouldn't be possible. The order IDs that are compared for order identification and synchronization purposes are the ones from the `id` field in both the `wp_wc_orders` table and the posts table.
If the posts table is authoritative, achieving an order ID match is easy: the record in `wp_wc_orders` is created with the same ID and that's it. However, when the new orders tables are authoritative there's a problem: the posts table is used to store multiple types of data, not only orders; and by the time synchronization needs to happen, a non-order post could already exist having the same ID as the order to synchronize.
To solve this, _placeholder records_ are used. Whenever the new orders tables are authoritative and immediate synchronization is disabled, creating a new order will cause a record with post type `shop_order_placehold` and the same ID as the order to be created in the posts table; this effectively "reserves" the order ID in the posts table. Then, at synchronization time, the record is filled appropriately and its post type is changed to `shop_order`.
## [Order Data Storage](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-8)
You can switch between data stores freely to sync the data between the tables.
- If you select **“WordPress Post Tables”**, the system will save the order data within `_post` and `_postmeta` tables. The order tables are not utilized in this scenario.
![Select WordPress Post Tables](https://woocommerce.com/wp-content/uploads/2023/10/image-18.png?w=650)
- If you select **“High-Performance Order Storage”**, the system will save the order data within the new WooCommerce order tables
![Select High-Performance Order Storage](https://woocommerce.com/wp-content/uploads/2023/10/image-19.png?w=650)
- If you select **“WordPress Post Tables”** and **“Enable compatibility mode”**, the system will sync the order data between the posts/postmeta and the WooCommerce order tables.
![Select WordPress Post Tables and Enable compatibility mode](https://woocommerce.com/wp-content/uploads/2023/10/image-20.png?w=650)
## [Incompatible Plugins](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-9)
If you are using a plugin that is not compatible with High-Performance Order Storage, then the HPOS option will be disabled under **WooCommerce > Settings > Advanced > Features**.
![Incompatible plugin](https://woocommerce.com/wp-content/uploads/2023/10/image-21.png?w=650)
- You can click on “**View and manage**” to review the list of incompatible plugins
- Or you can visit `https://example.com/wp-admin/plugins.php?plugin_status=incompatible_with_feature&feature_id=custom_order_tables` to review the list of incompatible plugins (please replace `example.com` with your site domain)
![Plugins page](https://woocommerce.com/wp-content/uploads/2023/10/image-22.png?w=650)
> **Note:** If you are using a third-party extension that isnt working properly with High-Performance Order Storage then please notify the developers of the extension and ask them to update their extension to add support for HPOS. Its up to the extension developers to add support for HPOS. We have [developer resources and documentation](https://developer.woocommerce.com/2022/09/14/high-performance-order-storage-progress-report/) available to help with their integration efforts.
## [Disabling HPOS](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-10)
If you encounter problems or if you need to continue working with plugins that are not yet compatible with HPOS, then we recommend temporarily switching back to **WordPress posts storage**.
To do this, navigate to **WooCommerce ▸ Settings ▸ Advanced ▸ Features** and start by making sure that **compatibility mode** is enabled. If it was not already enabled, you may find you need to wait for some time while order data is synchronized across data-stores.
![WooCommerce ▸ Settings ▸ Advanced ▸ Features Screen](https://woocommerce.com/wp-content/uploads/2023/10/hpos-feature-settings.png?w=650)
Once synchronization has completed, you can select **WordPress posts storage (legacy)** as your preferred option. You can also disable compatibility mode at this point. Once you are ready to re-enable HPOS, simply follow the instructions posted at the [start of this doc](https://github.com/woocommerce/woocommerce/blob/trunk/docs/high-performance-order-storage/#section-3). Finally, remember to save this page between changes!
As noted earlier, we also strongly recommend reaching out to the support teams of any plugins that are incompatible, so they can take corrective action.

View File

@ -0,0 +1,25 @@
# How to enable HPOS
From WooCommerce 8.2, released on October 2023, HPOS is enabled by default for new installations. Existing stores can switch to the “High-Performance Order Storage” from “WordPress Posts Storage” by following the below steps.
To activate High-Performance Order Storage, existing stores will first need to get both the posts and orders table in sync, which can be done by turning on the setting “**Enable compatibility mode (synchronizes orders to the posts table)**“.
1/ Navigate to **WooCommerce > Settings > Advanced > Features**
2/ Turn on the **“Enable compatibility mode (synchronizes orders to the posts table)”** setting.
![Enable HPOS Screen](https://woocommerce.com/wp-content/uploads/2023/10/New-Project-4.jpg?w=650)
3/ Once this setting is activated, background actions will be scheduled.
- The action `wc_schedule_pending_batch_process` checks whether there are orders that need to be backfilled.
- If there are, it schedules another action `wc_run_batch_process` that actually backfills the orders to post storage.
- You can either wait for these actions to run on their own, which should be quite soon, or you can go to **WooCommerce > Status > Scheduled Actions**, find the actions and click on the run button.
- The action will backfill 25 orders at a time, if there are more orders to be synced, then more actions will be scheduled as soon as the previous actions are completed.
![wc_schedule_pending_batch_process Screen](https://woocommerce.com/wp-content/uploads/2023/10/2.jpg?w=650)
![wc_run_batch_process Screen](https://woocommerce.com/wp-content/uploads/2023/10/New-Project-5.jpg?w=650)
4/ After both tables are successfully synchronized, youll be able to select the option to switch to High-Performance Order Storage (HPOS).
- It is advisable to maintain compatibility mode for some time to ensure a seamless transition. In case of any issues, reverting to the post table can be done instantly.

View File

@ -1,86 +0,0 @@
# High Performance Order Storage (HPOS)
WooCommerce has traditionally stored store orders and related order information (like refunds) as custom WordPress post types or post meta records. This comes with performance issues, and that's why HPOS (High-Performance Order Storage) was developed. HPOS is the WooCommerce engine that stores orders in dedicated tables.
HPOS is also referred to as COT (Custom Order Tables) in some parts of the code, that's the early name of the engine.
There are a number of settings that control HPOS operation. Boolean settings are stored using the usual WooCommerce convention: `yes` for enabled ("set to true"), `no` for disabled ("set to false").
Most of the code related to HPOS is in [src/Internal/DataStores/Orders](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/src/Internal/DataStores/Orders).
## Database tables
A number of database tables are used to store order data by HPOS. The `get_all_table_names` method in [the OrdersTableDataStore class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) will return the names of all the tables.
## Enabling the feature
For HPOS to be usable, the HPOS feature must first be enabled. This should be done programmatically via [the features controller](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/Features/FeaturesController.php), or via admin UI (WooCommerce - Settings - Advanced - Features). The feature enable option name for HPOS is `woocommerce_feature_custom_order_tables_enabled`. The required database tables will be created automatically once the feature is enabled.
## Authoritative tables
At any given time, while the HPOS feature is enabled, there are two roles for the involved database tables: _authoritative_ and _backup_. The authoritative tables are the working tables, where order data will be stored to and retrieved from during normal operation of the store. The _backup_ tables will receive a copy of the authoritative data whenever [synchronization](#synchronization) happens.
If the `woocommerce_custom_orders_table_enabled` options is set to true, HPOS is active and [the new tables](#database-tables) are authoritative, while the posts and post meta tables act as the backup tables. If the option is set to false, it's the other way around. The option can be changed via admin UI (WooCommerce - Settings - Advanced - Custom data stores).
[The CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php) hooks on the `woocommerce_order_data_store` filter so that `WC_Data_Store::load( 'order' );` will return either an instance of [OrdersTableDataStore](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php) or an instance of [WC_Order_Data_Store_CPT](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php), depending on which are the authoritative tables.
In order to preserve data integrity, switching the authoritative tables (from the new tables to the posts table or the other way around) isn't allowed while there are orders pending synchronization.
## Synchronization
_Synchronization_ is the process of applying all the pending changes in the authoritative tables to the backup tables. _Orders pending synchronization_ are orders that have been modified in the authoritative tables but the changes haven't been applied to the backup tables yet.
This can happen in a number of ways:
### Immediate synchronization
If the `woocommerce_custom_orders_table_data_sync_enabled` setting is set to true, synchronization happens automatically and immediately as soon as the orders are changed in the authoritative tables.
### Manual synchronization
When immediate synchronization is disabled, it can be triggered manually via command line as follows: `wp wc cot sync`. It can also be triggered programmatically as follows:
```php
$synchronizer = wc_get_container()->get(Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::class);
$order_ids = $synchronizer->get_next_batch_to_process( $batch_size );
if ( count( $order_ids ) ) {
$synchronizer->process_batch( $order_ids );
}
```
where `$batch_size` is the maximum count of orders to process.
### Scheduled synchronization
If immediate synchronization gets activated (`woocommerce_custom_orders_table_data_sync_enabled` is set to true) while there are orders pending synchronization, an instance of [DataSynchronizer](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/DataSynchronizer.php) will be enqueued using [BatchProcessingController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/BatchProcessing/BatchProcessingController.php) so that the synchronization of created/modified/deleted orders will happen in batches via scheduled actions. This scheduling happens inside [CustomOrdersTableController](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php), by means of hooking into `woocommerce_update_options_advanced_custom_data_stores`.
If for some reason immediate synchronization is already active but synchronization is not scheduled, a trick to restart it is to go to the settings page (WooCommerce - Settings - Advanced - Custom data stores) and hit "Save" even without making any changes. As long as "Keep the posts table and the orders tables synchronized" is checked the synchronization scheduling will happen, even if it was checked before.
If the `woocommerce_auto_flip_authoritative_table_roles` option is set to true (there's a checkbox for it in the settings page), the authoritative tables will be switched automatically once all the orders have been synchronized. This is handled by [the CustomOrdersTableController class](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php).
### Deletion synchronization
Synchronization of order deletions is tricky: if an order exists in one set of tables (new tables or posts) but not in the other, it's not clear if the missing orders need to be created or if the existing orders need to be deleted. Theoretically, the orders missing from the backup tables imply the former and the orders missing from the authoritative tables imply the latter; but that's dangerous as a bug in the involved code could easily lead to the deletion of legitimate orders.
To achieve a robust order deletion synchronization mechanism the following is done. Whenever an order is deleted and immediate synchronization is disabled, a record is created in the `wp_wc_orders_meta` table that has `deleted_from` as the key and the name of the authoritative table the order was deleted from (`wp_wc_orders` or the posts table). Then at synchronization time these records are processed (the corresponding orders are deleted from the corresponding tables) and deleted afterwards.
An exception to the above are the [placeholder records](#placeholder-records): these are deleted immediately when the corresponding order is deleted from `wp_wc_orders`, even if immediate synchronization is disabled.
## Placeholder records
Order IDs must match in both the authoritative tables and the backup tables, otherwise synchronization wouldn't be possible. The order IDs that are compared for order identification and synchronization purposes are the ones from the `id` field in both the `wp_wc_orders` table and the posts table.
If the posts table is authoritative, achieving an order ID match is easy: the record in `wp_wc_orders` is created with the same ID and that's it. However, when the new orders tables are authoritative there's a problem: the posts table is used to store multiple types of data, not only orders; and by the time synchronization needs to happen, a non-order post could already exist having the same ID as the order to synchronize.
To solve this, _placeholder records_ are used. Whenever the new orders tables are authoritative and immediate synchronization is disabled, creating a new order will cause a record with post type `shop_order_placehold` and the same ID as the order to be created in the posts table; this effectively "reserves" the order ID in the posts table. Then, at synchronization time, the record is filled appropriately and its post type is changed to `shop_order`.

View File

@ -1,5 +0,0 @@
# High Performance Order Storage
> ⚠️ **Notice:** This documentation is currently a **work in progress**. While it's open to the public for transparency and collaboration, please be aware that some sections might be incomplete or subject to change. We appreciate your patience and welcome any contributions!
This section is where you can learn about High-Performance Order Storage (HPOS): a new database storage for orders to allow effortless scaling for large and high growth stores.

View File

@ -0,0 +1,160 @@
# Translating WooCommerce
WooCommerce is already translated into several languages and is translation-ready right out of the box. All thats 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 youre 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 dont show up translated on the front end. Why?
If some of your translated strings dont 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)

View File

@ -0,0 +1,276 @@
# WooCommerce core critical flows
We have identified what we consider to be our most critical user flows within WooCommerce Core. These flows will help us focus and prioritize our testing efforts. They will also help us consider the impact of changes and priority of issues.
These flows will continually evolve as the platform evolves with flows updated, added or re-prioritised.
## Shopper critical flow areas
- 🛒 [Shopper > Shop](#shopper---shop)
- 🛒 [Shopper > Product](#shopper---product)
- 🛒 [Shopper > Cart](#shopper---cart)
- 🛒 [Shopper > Checkout](#shopper---checkout)
- 🛒 [Shopper > Email](#shopper---email)
- 🛒 [Shopper > My Account](#shopper---my-account)
## Merchant critical flow areas
- 💳 [Merchant > Onboarding](#merchant---onboarding)
- 💳 [Merchant > Dashboard](#merchant---dashboard)
- 💳 [Merchant > Settings](#merchant---settings)
- 💳 [Merchant > Coupons](#merchant---coupons)
- 💳 [Merchant > Marketing](#merchant---marketing)
- 💳 [Merchant > Analytics](#merchant---analytics)
- 💳 [Merchant > Products](#merchant---products)
- 💳 [Merchant > Orders](#merchant---orders)
- 💳 [Merchant > Customers](#merchant---customers)
- 💳 [Merchant > Email](#merchant---email)
- 💳 [Merchant > Plugins](#merchant---plugins)
- 💳 [Merchant > My Subscriptions](#merchant---my-subscriptions)
- 💳 [Merchant > Pages](#merchant---pages)
- 💳 [Merchant > Posts](#merchant---posts)
### Shopper - Shop
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------------------------- | --------------------------------------- |
| Shopper | Shop | Search Store | shopper/shop-search-browse-sort.spec.js |
| Shopper | Shop | Browse by categories | shopper/shop-search-browse-sort.spec.js |
| Shopper | Shop | Can sort items | shopper/shop-search-browse-sort.spec.js |
| Shopper | Shop | Add Simple Product to Cart (from shop page) | shopper/cart.spec.js |
| Shopper | Shop | Display shop catalog | |
| Shopper | Shop | Products by tag | |
| Shopper | Shop | Products by attribute | |
| Shopper | Shop | Use product filters | |
| Shopper | Shop | Display product showcase blocks correctly | |
| Shopper | Shop | Navigation menu default links | |
### Shopper - Product
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ---------------------------------------------------- | -------------------------------- |
| Shopper | Product | Add Simple Product to Cart | shopper/product-simple.spec.js |
| Shopper | Product | Add Grouped Product to Cart | shopper/product-grouped.spec.js |
| Shopper | Product | Variable Product info updates depending on variation | shopper/product-variable.spec.js |
| Shopper | Product | Add Variable Product to Cart | shopper/product-variable.spec.js |
| Shopper | Product | Display up-sell product | |
| Shopper | Product | Display releated products | |
| Shopper | Product | Display reviews | |
| Shopper | Product | Add review | |
| Shopper | Product | View product images | |
| Shopper | Product | View product descriptions | |
### Shopper - Cart
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------------------------ | ---------------------------------- |
| Shopper | Cart | Add to cart redirects to cart when enabled | shopper/cart-redirection.spec.js |
| Shopper | Cart | View cart | shopper/cart.spec.js |
| Shopper | Cart | Update product quantity within limits | shopper/cart.spec.js |
| Shopper | Cart | Remove products from cart | shopper/cart.spec.js |
| Shopper | Cart | Apply all coupon types | shopper/cart-coupons.spec.js |
| Shopper | Cart | Display shipping options by address | shopper/calculate-shipping.spec.js |
| Shopper | Cart | View empty cart | |
| Shopper | Cart | Display correct tax | |
| Shopper | Cart | Respect coupon usage contraints | |
| Shopper | Cart | Display cross-sell products | |
| Shopper | Cart | Proceed to checkout | |
### Shopper - Checkout
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ---------------------------------------- | --------------------------------------- |
| Shopper | Checkout | Correct item in Order Review | shopper/checkout.spec.js |
| Shopper | Checkout | Can add shipping address | shopper/checkout.spec.js |
| Shopper | Checkout | Guest can place order | shopper/checkout.spec.js |
| Shopper | Checkout | Create an account | shopper/checkout-create-account.spec.js |
| Shopper | Checkout | Login to existing account | shopper/checkout-login.spec.js |
| Shopper | Checkout | Existing customer can place order | shopper/checkout.spec.js |
| Shopper | Checkout | Use all coupon types | shopper/checkout-coupons.spec.js |
| Shopper | Checkout | View checkout | |
| Shopper | Checkout | Receive warnings when form is incomplete | |
| Shopper | Checkout | Add billing address | |
| Shopper | Checkout | Respect coupon usage contraints | |
| Shopper | Checkout | Display correct tax in checkout | |
| Shopper | Checkout | View order confirmation page | |
### Shopper - Email
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------------------- | ------------------------------------- |
| Shopper | Email | Customer Account Emails Received | shopper/ |
| Shopper | Email | Customer Order Detail Emails Received | shopper/order-email-receiving.spec.js |
### Shopper - My Account
| User Type | Flow Area | Flow Name | Test File |
| --------- | ---------- | ------------------------- | ----------------------------------------- |
| Shopper | My Account | Create an account | shopper/my-account-create-account.spec.js |
| Shopper | My Account | Login to existing account | shopper/my-account.spec.js |
| Shopper | My Account | View Account Details | shopper/my-account.spec.js |
| Shopper | My Account | Update Addresses | shopper/my-account-addresses.spec.js |
| Shopper | My Account | View Orders | shopper/ |
| Shopper | My Account | Pay for Order | shopper/my-account-pay-order.spec.js |
| Shopper | My Account | View Downloads | shopper/my-account-downloads.spec.js |
### Merchant - Onboarding
| User Type | Flow Area | Flow Name | Test File |
| --------- | ------------- | -------------------------------------------------------------- | --------- |
| Merchant | Core Profiler | Introduction & opt-in | |
| Merchant | Core Profiler | User profile information | |
| Merchant | Core Profiler | Business information | |
| Merchant | Core Profiler | Extensions page | |
| Merchant | Core Profiler | WooPayments included in extensions for eligible criteria | |
| Merchant | Core Profiler | WooPayments not included in extensions for ineligible criteria | |
| Merchant | Core Profiler | Install all default extensions | |
| Merchant | Core Profiler | Complete site setup | |
| Merchant | Core Profiler | Skip introduction and confirm business location | |
| Merchant | Core Profiler | Completed profiler doesn't reappear after site upgrade | |
### Merchant - Dashboard
| User Type | Flow Area | Flow Name | Test File |
| --------- | -------------- | ------------------------------------------------------ | --------- |
| Merchant | WC Home | Completing profiler redirects to home | |
| Merchant | WC Home | Complete all steps on task list | |
| Merchant | WC Home | Hide the task list | |
| Merchant | WC Home | Store management displayed after task list finished | |
| Merchant | WC Home | Direct access to analytics reports from stats overview | |
| Merchant | WC Home | Preserve task list completion status after upgrade | |
| Merchant | WC Home | Interact with extended task list | |
| Merchant | Activity Panel | Interact with activity button | |
| Merchant | Inbox | Interact with notes and perform CTAs | |
| Merchant | Inbox | Dismiss single note and all notes | |
### Merchant - Settings
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ----------------------------------------------- | ---------------------------------------- |
| Merchant | Settings | Update General Settings | merchant/settings-general.spec.js |
| Merchant | Settings | Add Tax Rates | merchant/settings-tax.spec.js |
| Merchant | Settings | Add Shipping Zones | merchant/create-shipping-zones.spec.js |
| Merchant | Settings | Add Shipping Classes | merchant/create-shipping-classes.spec.js |
| Merchant | Settings | Enable local pickup for checkout block | |
| Merchant | Settings | Enable HPOS | |
| Merchant | Settings | Update payment settings | |
| Merchant | Settings | Maintain tax and shipping settings post-upgrade | |
### Merchant - Coupons
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | ------------------------------ |
| Merchant | Coupons | Add all coupon types | merchant/create-coupon.spec.js |
| Merchant | Coupons | Add restricted coupon | |
### Merchant - Marketing
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------- | --------- |
| Merchant | Marketing | Display marketing overview | |
### Merchant - Analytics
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------------------------------- | --------------------------------- |
| Merchant | Analytics | View revenue report | admin-analytics/analytics.spec.js |
| Merchant | Analytics | View overview report | |
| Merchant | Analytics | Confirm correct summary numbers on overview report | |
| Merchant | Analytics | Use date filter on overview page | |
| Merchant | Analytics | Customize performance indicators on overview page | |
| Merchant | Analytics | Use date filter on revenue report | |
| Merchant | Analytics | Download revenue report as CSV | |
| Merchant | Analytics | Use advanced filters on orders report | |
| Merchant | Analytics | Analytics settings | |
| Merchant | Analytics | Set custom date range on revenue report | |
### Merchant - Products
| User Type | Flow Area | Flow Name | Test File |
| --------- | -------------- | ------------------------------ | ---------------------------------------------------------------------- |
| Merchant | Products | View all products | |
| Merchant | Products | Search products | merchant/product-search.spec.js |
| Merchant | Products | Add simple product | merchant/create-simple-product.spec.js |
| Merchant | Products | Add variable product | merchant/products/add-variable-product/create-variable-product.spec.js |
| Merchant | Products | Edit product details | merchant/product-edit.spec.js |
| Merchant | Products | Add virtual product | merchant/create-simple-product.spec.js |
| Merchant | Products | Import products CSV | merchant/product-import-csv.spec.js |
| Merchant | Products | Add downloadable product | |
| Merchant | Products | View product reviews list | |
| Merchant | Products | View all products reviews list | |
| Merchant | Products | Edit product review | |
| Merchant | Products | Trash product review | |
| Merchant | Products | Bulk edit products | |
| Merchant | Products | Remove products | |
| Merchant | Products | Manage product images | |
| Merchant | Products | Manage product inventory | |
| Merchant | Products | Manage product attributes | |
| Merchant | Products | Manage global attributes | |
| Merchant | Products | Add up-sell | |
| Merchant | Products | Add cross-sell | |
| Merchant | Products (New) | Disable new product experience | |
| Merchant | Products (New) | Add simple product | |
| Merchant | Products (New) | Edit simple product | |
| Merchant | Products (New) | Manage product images | |
| Merchant | Products (New) | Manage product inventory | |
| Merchant | Products (New) | Manage product attributes | |
### Merchant - Orders
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ---------------------------------------------------------------- | -------------------------------------- |
| Merchant | Orders | View all orders | merchant/ |
| Merchant | Orders | Can add new order basic | merchant/order-edit.spec.js |
| Merchant | Orders | View single order | merchant/order-edit.spec.js |
| Merchant | Orders | Update order status to completed | merchant/order-edit.spec.js |
| Merchant | Orders | Update order status to cancelled | |
| Merchant | Orders | Update order details | merchant/order-edit.spec.js |
| Merchant | Orders | Customer payment page | merchant/customer-payment-page.spec.js |
| Merchant | Orders | Refund order | merchant/order-refund.spec.js |
| Merchant | Orders | Apply coupon | merchant/order-coupon.spec.js |
| Merchant | Orders | Can add new order complex - multiple product types & tax classes | merchant/create-order.spec.js |
| Merchant | Orders | Search orders | merchant/order-search.spec.js |
| Merchant | Orders | Filter orders by order status | merchant/order-status-filter.spec.js |
| Merchant | Orders | Bulk change order status | |
| Merchant | Orders | Add order notes | |
### Merchant - Customers
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | --------- |
| Merchant | Customers | Display customer list | |
### Merchant - Email
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | -------------------------------------------------- | ----------------------------- |
| Merchant | Email | Receive and check content of new order email | merchant/order-emails.spec.js |
| Merchant | Email | Receive and check content of cancelled order email | |
| Merchant | Email | Receive and check content of failed order email | |
| Merchant | Email | Resent new order email | |
| Merchant | Email | Send invoice/order details to customer via Email | |
### Merchant - Plugins
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | ------------------------- | -------------------------------------- |
| Merchant | Plugins | Can update WooCommerce | smoke-tests/update-woocommerce.spec.js |
| Merchant | Plugins | Can uninstall WooCommerce | |
### Merchant - My Subscriptions
| User Type | Flow Area | Flow Name | Test File |
| --------- | ---------------- | --------------------------------------- | --------- |
| Merchant | My Subscriptions | Can initiate WooCommerce.com Connection | |
### Merchant - Pages
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | ---------------------------- |
| Merchant | Pages | Can create a new page | merchant/create-page.spec.js |
### Merchant - Posts
| User Type | Flow Area | Flow Name | Test File |
| --------- | --------- | --------------------- | ---------------------------- |
| Merchant | Posts | Can create a new post | merchant/create-post.spec.js |

View File

@ -0,0 +1,62 @@
# CSS/Sass Naming Conventions
Table of Contents:
- [Introduction](#introduction)
- [Prefixing](#prefixing)
- [Class names](#class-names)
- [Example](#example)
- [TL;DR](#tldr)
## Introduction
Our guidelines are based on those used in [Calypso](https://github.com/Automattic/wp-calypso), which itself follows the [BEM methodology](https://getbem.com/).
Refer to the [Calypso CSS/Sass Coding Guidelines](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md) for full details.
Read more about [BEM key concepts](https://en.bem.info/methodology/key-concepts/).
There are a few differences in WooCommerce which are outlined below.
## Prefixing
As a WordPress plugin WooCommerce has to play nicely with WordPress core and other plugins/themes. To minimize conflict potential, all classes should be prefixed with `.woocommerce-`.
## Class names
When naming classes, remember:
- **Block** - Standalone entity that is meaningful on its own. Such as the name of a component.
- **Element** - Parts of a block and have no standalone meaning. They are semantically tied to its block.
- **Modifier** - Flags on blocks or elements. Use them to change appearance or behavior.
### Example
```css
/* Block */
.woocommerce-loop {}
/* Nested block */
.woocommerce-loop-product {}
/* Modifier */
.woocommerce-loop-product--sale {}
/* Element */
.woocommerce-loop-product__link {}
/* Element */
.woocommerce-loop-product__button-add-to-cart {}
/* Modifier */
.woocommerce-loop-product__button-add-to-cart--added {}
```
**Note:** `.woocommerce-loop-product` is not named as such because the block is nested within `.woocommerce-loop`. It's to be specific so that we can have separate classes for single products, cart products, etc. **Nested blocks do not need to inherit their parents full name.**
## TL;DR
- Follow the [WordPress Coding standards for CSS](https://make.wordpress.org/core/handbook/best-practices/coding-standards/css/) unless it contradicts anything here.
- Follow [Calypso guidelines for CSS](https://wpcalypso.wordpress.com/devdocs/docs/coding-guidelines/css.md).
- Use BEM for [class names](https://en.bem.info/methodology/naming-convention/).
- Prefix all class names.

View File

@ -0,0 +1,88 @@
# Naming Conventions
Table of Contents:
- [PHP](#php)
- [`/src`](#src)
- [`/includes`](#includes)
- [JS](#js)
- [CSS and SASS](#css-and-sass)
## PHP
WooCommerce core generally follows [WordPress PHP naming conventions](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions).
There are some additional conventions that apply, depending on the location of the code.
### `/src`
Classes defined inside `/src` follow the [PSR-4](https://www.php-fig.org/psr/psr-4/) standard. See the [README for `/src`](../../plugins/woocommerce/src/README.md) for more information.
The following conventions apply to this directory:
- No class name prefix is needed, as all classes in this location live within the `Automattic\WooCommerce` namespace.
- Classes are named using `CamelCase` convention.
- Functions are named using `snake_case` convention.
- Class file names should match the class name. They do not need a `class-` prefix.
- The namespace should match the directory structure.
- Hooks are prefixed with `woocommerce_`.
- Hooks are named using `snake_case` convention.
For example, the class defined in `src/Util/StringUtil.php` should be named `StringUtil` and should be in the `Automattic\WooCommerce\Util` namespace.
### `/includes`
The `/includes` directory contains legacy code that does not follow the PSR-4 standard. See the [README for `/includes`](../../plugins/woocommerce/includes/README.md) for more information.
The following conventions apply to this directory:
- Class names are prefixed with `WC_`.
- Classes are named using `Upper_Snake_Case` convention.
- Functions are prefixed with `wc_`.
- Functions are named using `snake_case` convention.
- Hooks are prefixed with `woocommerce_`.
- Hooks are named using `snake_case` convention.
Class name examples:
- `WC_Cache_Helper`
- `WC_Cart`
Function name examples:
- `wc_get_product()`
- `wc_is_active_theme()`
Hook name examples (actions or filters):
- `woocommerce_after_checkout_validation`
- `woocommerce_get_formatted_order_total`
## JS
WooCommerce core follows [WordPress JS naming conventions](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/#naming-conventions).
As with PHP, function, class, and hook names should be prefixed, but the convention for JS is slightly different.
- Global class names are prefixed with `WC`. Class names exported from modules are not prefixed.
- Classes are named using `UpperCamelCase` convention.
- Global function names are prefixed with `wc`. Function names exported from modules are not prefixed.
- Functions are named using `camelCase` convention.
- Hooks names are prefixed with `woocommerce`.
- Hooks are named using `camelCase` convention.
Global class name example:
- `WCOrdersTable`
Global function name example:
- `wcSettings()`
Hook name example (actions or filters):
- `woocommerceTracksEventProperties`
## CSS and SASS
See [CSS/Sass Naming Conventions](./css-sass-naming-conventions.md).

View File

@ -0,0 +1,22 @@
# Removing /product/ , /product-category/ , or /shop/ from the URLs
**Note:** This is a **Developer level** doc. If you are unfamiliar with code/templates and resolving potential conflicts, select a [WooExpert or Developer](https://woocommerce.com/customizations/) for assistance. We are unable to provide support for customizations under our [Support Policy](http://www.woocommerce.com/support-policy/).
## [In sum](https://github.com/woocommerce/woocommerce/blob/trunk/docs/quality-and-best-practices/removing-product-product-category-or-shop-from-the-urls.md#section-1)
Removing `/product/`, `/product-category/`, or `/shop/` from the URLs is not advisable due to the way WordPress resolves its URLs. It uses the `product-category` (or any other text for that matter) base of a URL to detect that it is a URL leading to a product category. There are SEO plugins that allow you to remove this base, but that can lead to a number of problems with performance and duplicate URLs.
## [Better to avoid](https://github.com/woocommerce/woocommerce/blob/trunk/docs/quality-and-best-practices/removing-product-product-category-or-shop-from-the-urls.md#section-2)
You will make it harder for WordPress to detect what page you are trying to reach when you type in a product category URL. Also, understand that the standard “Page” in WordPress always has no base text in the URL. For example:
- `http://yoursite.com/about-page/` (this is the URL of a standard page)
- `http://yoursite.com/product-category/category-x/` (this is the URL leading to a product category)
What would happen if we remove that product-category part?
- `http://yoursite.com/about-page/`
- `http://yoursite.com/category-x/`
WordPress will have to do much more work to detect what page you are looking for when entering one of the above URLs. That is why we do not recommend using any SEO plugin to achieve this.

View File

@ -0,0 +1,138 @@
# Writing high quality testing instructions
## Introduction
Having clear testing Instructions on pull requests is the first level of quality engineering in WooCommerce, which is key for testing early and minimizing the impact of unexpected effects in the upcoming versions of WooCommerce.
This page contains the following sections:
- [What is a test?](#what-is-a-test)
- [What to cover with the testing instructions](#what-to-cover-with-the-testing-instructions)
- [Flow to write good testing instructions](#flow-to-write-good-testing-instructions)
- [Examples](#examples)
## What is a test?
A test is a method that we can use to check that something meets certain criteria. It is typically defined as a procedure which contains the steps required to put the system under test in a certain state before executing the action to be checked. Therefore, a test consists of the following stages:
- **Preconditions:** All the steps that need to be performed to put the system in the desired state before executing the action we want to check. A test could have many preconditions.
- **Action:** This is the exact step that causes the change we want to check in the system. It should be only one because each test should ideally cover one thing at a time.
- **Validation:** Relates to the steps to be performed in order to validate the result of performing the action in the system. A test could validate more than one thing.
For example, in the process of adding an item to the cart:
- The **preconditions** would be all the steps involved in:
- The product creation process.
- Logging as a shopper.
- Heading to the shop page where the products are listed.
- The **action** would be clicking the _"Add to cart"_ button in the desired product.
- The **validation** stage would include checking that the cart icon (if any) shows 1 more item and the product we selected is now included in the cart.
Specifying the preconditions, actions and validations can be quite beneficial when understanding the scope of a test, because:
- The **preconditions** describe what we have to do so that we can execute the test successfully.
- The **action** lets us know the purpose of the test, in other words, it is the key to understanding what we need to test.
- The **validation** stage lets us know what to expect when executing the test.
In this context, we will refer to testing instructions as the tests we need to execute in order to validate that the changes delivered in a pull request or release work as expected. This means the testing instructions could refer to a test or more, involving the happy path and potential edge cases.
## What to cover with the testing instructions
As stated in the previous section, a test (in our context, a testing instruction) is a method to check that a new change or set of changes meets certain criteria.
Therefore, a PR could have testing instructions for multiple scenarios, in fact, it is recommended to include testing instructions for as many scenarios as needed to cover the changes introduced in the PR. In other words, please **add as many testing instructions as needed to cover the acceptance criteria**, understanding acceptance criteria as _the conditions that a software product must satisfy to be accepted by a user, customer or other stakeholders_ or, in the context of a PR, the conditions that this PR must satisfy to be accepted by users, developers and the WooCommerce community as per requirements.
## Flow to write good testing instructions
1. **Outline the user flows** you want to cover.
2. **Define the environment** where the testing instructions should be executed (server, PHP version, WP version, required plugins, etc), and start writing the testing instructions as if you were starting from a fresh install.
3. Identify the **preconditions**, **action** and **validation** steps.
4. Write **as many preconditions as you need** to explain how to set up the state of WooCommerce so that you can execute the desired action to test every flow.
1. Try to be detailed when explaining the interactions the user needs to perform in WooCommerce.
2. If there are several preconditions for a user flow that is explained in a public guide, feel free to simply link the guide in the testing instructions instead of writing several steps. For example, _"Enable dev mode in WooCommerce Payments by following the steps mentioned [here](https://woocommerce.com/document/woocommerce-payments/testing-and-troubleshooting/dev-mode/)"_.
5. Write **the action step**, which should cover the specific action that we want to test as part of this user flow.
6. Write **as many validation steps** as needed in order to assess that the actual result meets expectations.
1. Bear in mind to check only the steps needed to validate that this change works.
### Considerations for writing high-quality testing instructions
- Define the testing instructions in a way that they can be **understood and followed by everybody**, even for people new to WooCommerce.
- Make sure to describe every detail and **avoid assuming knowledge**, the spectrum of readers might be wide and some people would not know the concepts behind what is being assumed. For example, instead of saying _“Enable the [x] experiment”_, say something like:
```text
- Install the WooCommerce Beta Tester plugin.
- Go to `Tools > WCA Test Helper > Experiments`.
- Toggle the [x] experiment.
```
- Always try to explain in detail **where the user should head to**, for example instead of saying “Go to the Orders page as admin”, say “Go to [url]” or even “Go to WooCommerce > Orders”.
- Try to use real test data. For example, instead of saying _"Enter a name for the product"_, say something like _"Enter 'Blue T-Shirt' as the product name"_. This will make it more self-explanatory and remove potential doubts related to assuming knowledge.
- Make sure you **keep your testing instructions updated** if they become obsolete as part of a new commit.
- If the testing instructions require to add custom code, please **provide the code snippet**.
- If the testing instructions require to install a plugin, please **provide a link to this plugin, or the zip file** to install it.
- If the testing instructions require to hit an API endpoint, please provide the **link to the endpoint documentation**.
- Ideally **provide screenshots and/or videos** that supported what the testing instructions are explaining. If you are using links to collaborative tools then also provide an equivalent screenshot/video for those who do not have access.
## Examples
### Good quality testing instructions
#### Example 1
![Sample of good quality instructions](https://woocommerce.files.wordpress.com/2023/10/213682695-3dc51613-b836-4e7e-93ef-f75078ab48ac.png)
#### Example 2
![Another sample of good quality instructions](https://woocommerce.files.wordpress.com/2023/10/213682778-b552ab07-a518-48a7-9358-16adc5762aca.png)
### Improving real testing instructions
In this section, you will see some real examples of testing instructions that have room for improvement (before) and how we can tweak them (after).
Before:
![Instructions needing improvement](https://woocommerce.files.wordpress.com/2023/10/213682262-25bec5c3-154c-45ec-aa3d-d3e07f52669e.png)
After:
![Improved instructions](https://woocommerce.files.wordpress.com/2023/10/213682303-1b12ab97-f27a-41cb-a8db-da8a78d18840.png)
Improvements:
![Changes made](https://woocommerce.files.wordpress.com/2023/10/213682323-0ecc998d-69ab-4201-8daa-820b948315e8.png)
Before:
![Instructions needing improvement](https://woocommerce.files.wordpress.com/2023/10/213682396-8c52d20e-1fca-4ac1-8345-f381c15a102a.png)
After:
![Improved instructions](https://woocommerce.files.wordpress.com/2023/10/213682480-c01e0e84-5969-4456-8f43-70cbb8509e8d.png)
Improvements:
![Changes made](https://woocommerce.files.wordpress.com/2023/10/213682597-8d06e638-35dd-4ff8-9236-63c6ec5d05b8.jpg)
Before:
![Screenshot 2023-02-02 at 16 07 29](https://woocommerce.files.wordpress.com/2023/10/216365611-b540a814-3b8f-40f3-ae64-81018b9f97fb.png)
After:
![Screenshot 2023-02-02 at 16 22 31](https://woocommerce.files.wordpress.com/2023/10/216366043-967e5daa-6a23-4ab8-adda-5f3082d1ebf7.png)
Improvements:
![Screenshot 2023-02-02 at 16 09 24](https://woocommerce.files.wordpress.com/2023/10/216366152-b331648d-bcef-443b-b126-de2621a20862.png)
Before:
![Screenshot 2023-02-02 at 17 25 07](https://woocommerce.files.wordpress.com/2023/10/216388785-8806ea74-62e6-42da-8887-c8e291e7dfe2-1.png)
After:
![Screenshot 2023-02-02 at 17 49 22](https://woocommerce.files.wordpress.com/2023/10/216388842-e5ab433e-d288-4306-862f-72f6f81ab2cd.png)
Improvements:
![Screenshot 2023-02-02 at 17 39 23](https://woocommerce.files.wordpress.com/2023/10/216388874-c5b21fc3-f693-4a7e-a58a-c5d1b6606682.png)

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding feedback snackbar after image background removal

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add useBackgroundRemoval hook for image background removal API requests.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Reworking error handling and return value for useBackgroundRemoval hook.

View File

@ -1 +1,2 @@
export * from './useCompletion';
export * from './useBackgroundRemoval';

View File

@ -0,0 +1,165 @@
/**
* External dependencies
*/
import { renderHook, act } from '@testing-library/react-hooks/dom';
import { waitFor } from '@testing-library/react';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import {
BackgroundRemovalParams,
useBackgroundRemoval,
} from './useBackgroundRemoval';
import { requestJetpackToken } from '../utils/requestJetpackToken';
// Mocking the apiFetch function
jest.mock( '@wordpress/api-fetch', () =>
jest.fn().mockResolvedValue( {
blob: () =>
Promise.resolve(
new Blob( [ new ArrayBuffer( 51200 ) ], {
type: 'image/jpeg',
} )
),
} )
);
jest.mock( '../utils/requestJetpackToken' );
const mockedRequestJetpackToken = requestJetpackToken as jest.MockedFunction<
typeof requestJetpackToken
>;
describe( 'useBackgroundRemoval hook', () => {
let mockRequestParams: BackgroundRemovalParams;
beforeEach( () => {
jest.resetAllMocks();
// Initialize with valid parameters (50kb image file).
const imageFile = new File( [ new ArrayBuffer( 51200 ) ], 'test.png', {
type: 'image/png',
} );
mockRequestParams = {
imageFile,
};
mockedRequestJetpackToken.mockResolvedValue( { token: 'fake_token' } );
} );
it( 'should initialize with correct default values', () => {
const { result } = renderHook( () => useBackgroundRemoval() );
expect( result.current.imageData ).toBeNull();
expect( result.current.loading ).toBeFalsy();
} );
it( 'should return error on empty token', async () => {
mockedRequestJetpackToken.mockResolvedValue( { token: '' } );
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Invalid token' );
} );
it( 'should handle invalid file type', async () => {
mockRequestParams.imageFile = new File(
[ new ArrayBuffer( 51200 ) ],
'test.txt',
{ type: 'text/plain' }
);
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Invalid image file' );
} );
it( 'should return error on image file too small', async () => {
mockRequestParams.imageFile = new File(
[ new ArrayBuffer( 1024 ) ],
'test.png',
{ type: 'image/png' }
); // 1KB
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Image file too small, must be at least 5KB' );
} );
it( 'should return error on image file too large', async () => {
mockRequestParams.imageFile = new File(
[ new ArrayBuffer( 10240 * 1024 * 2 ) ],
'test.png',
{ type: 'image/png' }
); // 10MB
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'Image file too large, must be under 10MB' );
} );
it( 'should set loading to true when fetchImage is called', async () => {
(
apiFetch as jest.MockedFunction< typeof apiFetch >
).mockResolvedValue( {
blob: () =>
Promise.resolve(
new Blob( [ new ArrayBuffer( 51200 ) ], {
type: 'image/jpeg',
} )
),
} );
const { result } = renderHook( () => useBackgroundRemoval() );
await act( async () => {
result.current.fetchImage( mockRequestParams );
await waitFor( () =>
expect( result.current.loading ).toBeTruthy()
);
} );
expect( mockedRequestJetpackToken ).toHaveBeenCalled();
} );
it( 'should handle successful API call', async () => {
(
apiFetch as jest.MockedFunction< typeof apiFetch >
).mockResolvedValue( {
blob: () =>
Promise.resolve(
new Blob( [ new ArrayBuffer( 51200 ) ], {
type: 'image/jpeg',
} )
),
} );
const { result } = renderHook( () => useBackgroundRemoval() );
await act( async () => {
await result.current.fetchImage( mockRequestParams );
} );
expect( result.current.loading ).toBe( false );
expect( result.current.imageData ).toBeInstanceOf( Blob );
} );
it( 'should handle API errors', async () => {
(
apiFetch as jest.MockedFunction< typeof apiFetch >
).mockImplementation( () => {
throw new Error( 'API Error' );
} );
const { result } = renderHook( () => useBackgroundRemoval() );
await expect(
act( async () => {
await result.current.fetchImage( mockRequestParams );
} )
).rejects.toThrow( 'API Error' );
await waitFor( () => expect( result.current.loading ).toBeFalsy() );
expect( result.current.imageData ).toBe( null );
} );
} );

View File

@ -0,0 +1,85 @@
/**
* External dependencies
*/
import { useState } from 'react';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { createExtendedError } from '../utils/create-extended-error';
import { requestJetpackToken } from '../utils/requestJetpackToken';
export type BackgroundRemovalParams = {
imageFile: File;
};
type BackgroundRemovalResponse = {
loading: boolean;
imageData: Blob | null;
fetchImage: ( params: BackgroundRemovalParams ) => Promise< Blob >;
};
export const useBackgroundRemoval = (): BackgroundRemovalResponse => {
const [ loading, setLoading ] = useState( false );
const [ imageData, setImageData ] = useState< Blob | null >( null );
const fetchImage = async ( params: BackgroundRemovalParams ) => {
setLoading( true );
const { imageFile } = params;
const { token } = await requestJetpackToken();
if ( ! token ) {
throw createExtendedError( 'Invalid token', 'invalid_jwt' );
}
// Validate that the file is an image and has actual content.
if ( ! imageFile.type.startsWith( 'image/' ) ) {
throw createExtendedError(
'Invalid image file',
'invalid_image_file'
);
}
const fileSizeInKB = params.imageFile.size / 1024;
if ( fileSizeInKB < 5 ) {
throw createExtendedError(
'Image file too small, must be at least 5KB',
'image_file_too_small'
);
}
// The WPCOM REST API endpoint has a 10MB image size limit.
if ( fileSizeInKB > 10240 ) {
throw createExtendedError(
'Image file too large, must be under 10MB',
'image_file_too_large'
);
}
const formData = new FormData();
formData.append( 'image_file', imageFile );
formData.append( 'token', token );
try {
const response = await apiFetch( {
url: 'https://public-api.wordpress.com/wpcom/v2/ai-background-removal',
method: 'POST',
body: formData,
parse: false,
credentials: 'omit',
} );
const blob = await (
response as { blob: () => Promise< Blob > }
).blob();
setImageData( blob );
return blob;
} catch ( err ) {
throw err;
} finally {
setLoading( false );
}
};
return { loading, imageData, fetchImage };
};

View File

@ -4,6 +4,7 @@
export {
useCompletion as __experimentalUseCompletion,
UseCompletionError,
useBackgroundRemoval as __experimentalUseBackgroundRemoval,
} from './hooks';
/**

View File

@ -1,2 +1,3 @@
export * from './text-completion';
export * from './create-extended-error';
export * from './requestJetpackToken';

View File

@ -0,0 +1,87 @@
/**
* External dependencies
*/
import debugFactory from 'debug';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { createExtendedError } from './create-extended-error';
export const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
export const JWT_TOKEN_ID = 'jetpack-ai-jwt-token';
export const JWT_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000;
declare global {
interface Window {
JP_CONNECTION_INITIAL_STATE: {
apiNonce: string;
siteSuffix: string;
connectionStatus: { isActive: boolean };
};
}
}
/**
* Request a token from the Jetpack site to use with the API
*
* @return {Promise<{token: string, blogId: string}>} The token and the blogId
*/
export async function requestJetpackToken() {
const token = localStorage.getItem( JWT_TOKEN_ID );
let tokenData;
if ( token ) {
try {
tokenData = JSON.parse( token );
} catch ( err ) {
debugToken( 'Error parsing token', err );
throw createExtendedError(
'Error parsing cached token',
'token_parse_error'
);
}
}
if ( tokenData && tokenData?.expire > Date.now() ) {
debugToken( 'Using cached token' );
return tokenData;
}
const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
try {
const data: { token: string; blog_id: string } = await apiFetch( {
path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
credentials: 'same-origin',
headers: {
'X-WP-Nonce': apiNonce,
},
method: 'POST',
} );
const newTokenData = {
token: data.token,
blogId: siteSuffix,
/**
* Let's expire the token in 2 minutes
*/
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
};
debugToken( 'Storing new token' );
localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
return newTokenData;
} catch ( e ) {
throw createExtendedError(
'Error fetching new token',
'token_fetch_error'
);
}
}

View File

@ -1,89 +1,7 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
import debugFactory from 'debug';
/**
* Internal dependencies
*/
import { createExtendedError } from './create-extended-error';
const debugToken = debugFactory( 'jetpack-ai-assistant:token' );
const JWT_TOKEN_ID = 'jetpack-ai-jwt-token';
const JWT_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000;
declare global {
interface Window {
JP_CONNECTION_INITIAL_STATE: {
apiNonce: string;
siteSuffix: string;
connectionStatus: { isActive: boolean };
};
}
}
/**
* Request a token from the Jetpack site to use with the API
*
* @return {Promise<{token: string, blogId: string}>} The token and the blogId
*/
export async function requestJetpackToken() {
const token = localStorage.getItem( JWT_TOKEN_ID );
let tokenData;
if ( token ) {
try {
tokenData = JSON.parse( token );
} catch ( err ) {
debugToken( 'Error parsing token', err );
throw createExtendedError(
'Error parsing cached token',
'token_parse_error'
);
}
}
if ( tokenData && tokenData?.expire > Date.now() ) {
debugToken( 'Using cached token' );
return tokenData;
}
const apiNonce = window.JP_CONNECTION_INITIAL_STATE?.apiNonce;
const siteSuffix = window.JP_CONNECTION_INITIAL_STATE?.siteSuffix;
try {
const data: { token: string; blog_id: string } = await apiFetch( {
path: '/jetpack/v4/jetpack-ai-jwt?_cacheBuster=' + Date.now(),
credentials: 'same-origin',
headers: {
'X-WP-Nonce': apiNonce,
},
method: 'POST',
} );
const newTokenData = {
token: data.token,
blogId: siteSuffix,
/**
* Let's expire the token in 2 minutes
*/
expire: Date.now() + JWT_TOKEN_EXPIRATION_TIME,
};
debugToken( 'Storing new token' );
localStorage.setItem( JWT_TOKEN_ID, JSON.stringify( newTokenData ) );
return newTokenData;
} catch ( e ) {
throw createExtendedError(
'Error fetching new token',
'token_fetch_error'
);
}
}
import { requestJetpackToken } from './requestJetpackToken';
/**
* Leaving this here to make it easier to debug the streaming API calls for now

View File

@ -2,6 +2,35 @@
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [12.2.0](https://www.npmjs.com/package/@woocommerce/components/v/12.2.0) - 2023-10-17
- Patch - Add class back in for increase specificity of css for dropdown button. [#40494]
- Minor - Categories dropdown display error #39810 [#39811]
- Patch - Fixed empty component logo color, used generic rather than old pink [#39182]
- Patch - Fix invalid focus state of the experimental select control [#40519]
- Minor - Fix new category name field [#39857]
- Patch - Fix select control dropdown menu double scroll and width [#39989]
- Minor - Select attribute after pressing their names #39456 [#39574]
- Patch - TreeSelectControl Component - Make sure individuallySelectParent prop is respected [#40301]
- Minor - Add AI wizard business info step for Customize Your Store task [#39979]
- Minor - Add customize store assembler hub onboarding tour [#39981]
- Minor - Add ProgressBar component [#39979]
- Minor - Add tags (or general taxonomy ) block [#39966]
- Minor - Add Tooltip to each list item when need it [#39770]
- Minor - An international phone number input with country selection, and mobile phone numbers validation. [#40335]
- Minor - Image gallery and media uploader now support initial selected images. [#40633]
- Minor - Refactor Pagination component and split out into multiple re-usable components. Also added a `usePagination` hook. [#39967]
- Minor - Set button optional in MediaUploader component [#40526]
- Minor - Update ImageGallery block toolbar, moving some options to an ellipsis dropdown menu. [#39753]
- Minor - Allow users to select multiple items from the media library while adding images #39741 [#39741]
- Patch - Make eslint emit JSON report for annotating PRs. [#39704]
- Minor - Update pnpm to 8.6.7 [#39245]
- Patch - Upgraded Storybook to 6.5.17-alpha.0 for TypeScript 5 compatibility [#39745]
- Minor - Upgrade TypeScript to 5.1.6 [#39531]
- Patch - Add z-index=1 to tour-kit close btn to ensure it's clickable [#40456]
- Minor - Remove unnecessary use of woocommerce-page selector for DropdownButton styling. [#40218]
- Patch - Small condition change in the date time picker to avoid edge case where inputControl is null. [#40642]
## [12.1.0](https://www.npmjs.com/package/@woocommerce/components/v/12.1.0) - 2023-07-13
- Patch - Altering styles to correctly target fields within slot fills on product editor. [#36500]
@ -157,7 +186,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Minor - Fix DateTimePickerControl's onChange date arg to only be a string (TypeScript). [#35140]
- Minor - Improve experimental SelectControl accessibility [#35140]
- Minor - Improve Sortable component acessibility [#35140]
- - Create new experimental SelectControl component [#35140]
- Major - Create new experimental SelectControl component [#35140]
## [10.3.0](https://www.npmjs.com/package/@woocommerce/components/v/10.3.0) - 2022-08-12

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add tags (or general taxonomy ) block

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add Tooltip to each list item when need it

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add AI wizard business info step for Customize Your Store task

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix select control dropdown menu double scroll and width

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Image gallery and media uploader now support initial selected images.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: tweak
Small condition change in the date time picker to avoid edge case where inputControl is null.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Add customize store assembler hub onboarding tour

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Allow users to select multiple items from the media library while adding images #39741

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Make eslint emit JSON report for annotating PRs.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Update pnpm to 8.6.7

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Upgraded Storybook to 6.5.17-alpha.0 for TypeScript 5 compatibility

View File

@ -1,4 +0,0 @@
Significance: minor
Type: dev
Upgrade TypeScript to 5.1.6

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Fix badge size issue when a number larger than 3 digits is used.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: update
Update ImageGallery block toolbar, moving some options to an ellipsis dropdown menu.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: fix
Select attribute after pressing their names #39456

View File

@ -1,4 +0,0 @@
Significance: minor
Type: tweak
Remove unnecessary use of woocommerce-page selector for DropdownButton styling.

View File

@ -1,4 +0,0 @@
Significance: minor
Type: fix
Categories dropdown display error #39810

View File

@ -1,4 +0,0 @@
Significance: minor
Type: fix
Fix new category name field

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fix invalid focus state of the experimental select control

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Fixed empty component logo color, used generic rather than old pink

View File

@ -1,5 +0,0 @@
Significance: patch
Type: fix
Comment: Decode HTML escaped string for tree-item and selected-items components

View File

@ -1,4 +0,0 @@
Significance: patch
Type: tweak
Add z-index=1 to tour-kit close btn to ensure it's clickable

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
Add class back in for increase specificity of css for dropdown button.

View File

@ -1,5 +0,0 @@
Significance: patch
Type: dev
Comment: Applied lint auto fixes across monorepo

View File

@ -1,5 +0,0 @@
Significance: patch
Type: fix
Comment: Decode html characters in SelectTree

View File

@ -1,4 +0,0 @@
Significance: patch
Type: fix
TreeSelectControl Component - Make sure individuallySelectParent prop is respected

View File

@ -1,5 +0,0 @@
Significance: patch
Type: fix
Comment: Fixes and earlier unreleaed change, not changelog required

View File

@ -1,4 +0,0 @@
Significance: minor
Type: add
Refactor Pagination component and split out into multiple re-usable components. Also added a `usePagination` hook.

View File

@ -1,5 +0,0 @@
Significance: patch
Type: tweak
Comment: Just a minor README change.

View File

@ -1,4 +0,0 @@
Significance: patch
Type: dev
Comment: This is just a change to developer commands.

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/components",
"version": "12.1.0",
"version": "12.2.0",
"description": "UI components for WooCommerce.",
"author": "Automattic",
"license": "GPL-3.0-or-later",

View File

@ -8,6 +8,7 @@
font-size: 14px;
line-height: 27px;
align-items: center;
width: 32px;
height: 28px;
min-width: 32px;
height: 28px;
padding: 0 6px;
}

View File

@ -104,6 +104,7 @@ export {
SelectTreeMenuSlot as __experimentalSelectTreeMenuSlot,
} from './experimental-select-tree-control';
export { default as TreeSelectControl } from './tree-select-control';
export { default as PhoneNumberInput } from './phone-number-input';
// Exports below can be removed once the @woocommerce/product-editor package is released.
export {

View File

@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { Button, DropZone, FormFileUpload } from '@wordpress/components';
import { createElement } from 'react';
import { Fragment, createElement } from 'react';
import {
MediaItem,
MediaUpload,
@ -57,14 +57,11 @@ export const MediaUploader = ( {
onSelect = () => null,
uploadMedia = wpUploadMedia,
}: MediaUploaderProps ) => {
const getFormFileUploadAcceptedFiles = () =>
allowedMediaTypes.map( ( type ) => `${ type }/*` );
const multiple = Boolean( multipleSelect );
return (
<FormFileUpload
accept={ getFormFileUploadAcceptedFiles().toString() }
accept={ allowedMediaTypes.toString() }
multiple={ multiple }
onChange={ ( { currentTarget } ) => {
uploadMedia( {
@ -106,17 +103,21 @@ export const MediaUploader = ( {
allowedTypes={ allowedMediaTypes }
// @ts-expect-error - TODO multiple also accepts string.
multiple={ multipleSelect }
render={ ( { open } ) => (
<Button
variant="secondary"
onClick={ () => {
onMediaGalleryOpen();
open();
} }
>
{ buttonText }
</Button>
) }
render={ ( { open } ) =>
buttonText ? (
<Button
variant="secondary"
onClick={ () => {
onMediaGalleryOpen();
open();
} }
>
{ buttonText }
</Button>
) : (
<Fragment />
)
}
/>
{ hasDropZone && (

View File

@ -0,0 +1,32 @@
# PhoneNumberInput
An international phone number input with a country code select and a phone textfield which supports numbers, spaces and hyphens. And returns the full number as it is, in E.164 format, and the selected country alpha2.
Includes mobile phone numbers validation.
## Usage
```jsx
<PhoneNumberInput
value={ phoneNumber }
onChange={ ( value, e164, country ) => setState( value ) }
/>
```
### Props
| Name | Type | Default | Description |
| ---------------- | -------- | ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `value` | String | `undefined` | (Required) Phone number with spaces and hyphens |
| `onChange` | Function | `undefined` | (Required) Callback function when the value changes |
| `id` | String | `undefined` | ID for the input element, to bind a `<label>` |
| `className` | String | `undefined` | Additional class name applied to parent `<div>` |
| `selectedRender` | Function | `defaultSelectedRender` | Render function for the selected country, displays the country flag and code by default. |
| `itemRender` | Function | `itemRender` | Render function for each country in the dropdown, displays the country flag, name, and code by default. |
| `arrowRender` | Function | `defaultArrowRender` | Render function for the dropdown arrow, displays a chevron down icon by default. |
### `onChange` params
- `value`: Phone number with spaces and hyphens. e.g. `+1 234-567-8901`
- `e164`: Phone number in E.164 format. e.g. `+12345678901`
- `country`: Country alpha2 code. e.g. `US`

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
import React from 'react';
import { createElement, Fragment } from '@wordpress/element';
import { Icon, chevronDown } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { Country } from './utils';
const Flag: React.FC< { alpha2: string; src: string } > = ( {
alpha2,
src,
} ) => (
<img
alt={ `${ alpha2 } flag` }
src={ src }
className="wcpay-component-phone-number-input__flag"
/>
);
export const defaultSelectedRender = ( { alpha2, code, flag }: Country ) => (
<>
<Flag alpha2={ alpha2 } src={ flag } />
{ ` +${ code }` }
</>
);
export const defaultItemRender = ( { alpha2, name, code, flag }: Country ) => (
<>
<Flag alpha2={ alpha2 } src={ flag } />
{ `${ name } +${ code }` }
</>
);
export const defaultArrowRender = () => (
<Icon icon={ chevronDown } size={ 18 } />
);

View File

@ -0,0 +1,223 @@
/**
* External dependencies
*/
import {
createElement,
useState,
useRef,
useLayoutEffect,
} from '@wordpress/element';
import { useSelect } from 'downshift';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import data from './data';
import {
parseData,
Country,
sanitizeInput,
guessCountryKey,
numberToE164,
} from './utils';
import {
defaultSelectedRender,
defaultItemRender,
defaultArrowRender,
} from './defaults';
interface Props {
/**
* Phone number with spaces and hyphens.
*/
value: string;
/**
* Callback function when the value changes.
*
* @param value Phone number with spaces and hyphens. e.g. `+1 234-567-8901`
* @param e164 Phone number in E.164 format. e.g. `+12345678901`
* @param country Country alpha2 code. e.g. `US`
*/
onChange: ( value: string, e164: string, country: string ) => void;
/**
* ID for the input element, to bind a `<label>`.
*
* @default undefined
*/
id?: string;
/**
* Additional class name applied to parent `<div>`.
*
* @default undefined
*/
className?: string;
/**
* Render function for the selected country.
* Displays the country flag and code by default.
*
* @default defaultSelectedRender
*/
selectedRender?: ( country: Country ) => React.ReactNode;
/**
* Render function for each country in the dropdown.
* Displays the country flag, name, and code by default.
*
* @default defaultItemRender
*/
itemRender?: ( country: Country ) => React.ReactNode;
/**
* Render function for the dropdown arrow.
* Displays a chevron down icon by default.
*
* @default defaultArrowRender
*/
arrowRender?: () => React.ReactNode;
}
const { countries, countryCodes } = parseData( data );
/**
* An international phone number input with a country code select and a phone textfield which supports numbers, spaces and hyphens. And returns the full number as it is, in E.164 format, and the selected country alpha2.
*/
const PhoneNumberInput: React.FC< Props > = ( {
value,
onChange,
id,
className,
selectedRender = defaultSelectedRender,
itemRender = defaultItemRender,
arrowRender = defaultArrowRender,
} ) => {
const menuRef = useRef< HTMLButtonElement >( null );
const inputRef = useRef< HTMLInputElement >( null );
const [ menuWidth, setMenuWidth ] = useState( 0 );
const [ countryKey, setCountryKey ] = useState(
guessCountryKey( value, countryCodes )
);
useLayoutEffect( () => {
if ( menuRef.current ) {
setMenuWidth( menuRef.current.offsetWidth );
}
}, [ menuRef, countryKey ] );
const phoneNumber = sanitizeInput( value )
.replace( countries[ countryKey ].code, '' )
.trimStart();
const handleChange = ( code: string, number: string ) => {
// Return value, phone number in E.164 format, and country alpha2 code.
number = `+${ countries[ code ].code } ${ number }`;
onChange( number, numberToE164( number ), code );
};
const handleSelect = ( code: string ) => {
setCountryKey( code );
handleChange( code, phoneNumber );
};
const handleInput = ( event: React.ChangeEvent< HTMLInputElement > ) => {
handleChange( countryKey, sanitizeInput( event.target.value ) );
};
const handleKeyDown = (
event: React.KeyboardEvent< HTMLInputElement >
) => {
const pos = inputRef.current?.selectionStart || 0;
const newValue =
phoneNumber.slice( 0, pos ) + event.key + phoneNumber.slice( pos );
if ( /[- ]{2,}/.test( newValue ) ) {
event.preventDefault();
}
};
const {
isOpen,
getToggleButtonProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect( {
id,
items: Object.keys( countries ),
initialSelectedItem: countryKey,
itemToString: ( item ) => countries[ item || '' ].name,
onSelectedItemChange: ( { selectedItem } ) => {
if ( selectedItem ) handleSelect( selectedItem );
},
stateReducer: ( state, { changes } ) => {
if ( state.isOpen === true && changes.isOpen === false ) {
inputRef.current?.focus();
}
return changes;
},
} );
return (
<div
className={ classNames(
className,
'wcpay-component-phone-number-input'
) }
>
<button
{ ...getToggleButtonProps( {
ref: menuRef,
type: 'button',
className: classNames(
'wcpay-component-phone-number-input__button'
),
} ) }
>
{ selectedRender( countries[ countryKey ] ) }
<span
className={ classNames(
'wcpay-component-phone-number-input__button-arrow',
{ invert: isOpen }
) }
>
{ arrowRender() }
</span>
</button>
<input
id={ id }
ref={ inputRef }
type="text"
value={ phoneNumber }
onKeyDown={ handleKeyDown }
onChange={ handleInput }
className="wcpay-component-phone-number-input__input"
style={ { paddingLeft: `${ menuWidth }px` } }
/>
<ul
{ ...getMenuProps( {
'aria-hidden': ! isOpen,
className: 'wcpay-component-phone-number-input__menu',
} ) }
>
{ isOpen &&
Object.keys( countries ).map( ( key, index ) => (
// eslint-disable-next-line react/jsx-key
<li
{ ...getItemProps( {
key,
index,
item: key,
className: classNames(
'wcpay-component-phone-number-input__menu-item',
{ highlighted: highlightedIndex === index }
),
} ) }
>
{ itemRender( countries[ key ] ) }
</li>
) ) }
</ul>
</div>
);
};
export default PhoneNumberInput;

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import React, { useState } from 'react';
/**
* Internal dependencies
*/
import PhoneNumberInput from '../';
import { validatePhoneNumber } from '../validation';
export default {
title: 'WooCommerce Admin/components/PhoneNumberInput',
component: PhoneNumberInput,
};
const PNI: React.FC<
Partial< React.ComponentPropsWithoutRef< typeof PhoneNumberInput > >
> = ( { children, onChange, ...rest } ) => {
const [ phone, setPhone ] = useState( '' );
const [ output, setOutput ] = useState( '' );
const handleChange = ( value, i164, country ) => {
setPhone( value );
setOutput( JSON.stringify( { value, i164, country }, null, 2 ) );
onChange?.( value, i164, country );
};
return (
<>
<PhoneNumberInput
{ ...rest }
value={ phone }
onChange={ handleChange }
/>
{ children }
<pre>{ output }</pre>
</>
);
};
export const Examples = () => {
const [ valid, setValid ] = useState( false );
const handleValidation = ( _, i164, country ) => {
setValid( validatePhoneNumber( i164, country ) );
};
return (
<>
<h2>Basic</h2>
<PNI />
<h2>Labeled</h2>
<label htmlFor="pniID">Phone number</label>
<br />
<PNI id="pniID" />
<h2>Validation</h2>
<PNI onChange={ handleValidation }>
<pre>valid: { valid.toString() }</pre>
</PNI>
<h2>Custom renders</h2>
<PNI
arrowRender={ () => '🔻' }
itemRender={ ( { name, code } ) => `+${ code }:${ name }` }
selectedRender={ ( { alpha2, code } ) =>
`+${ code }:${ alpha2 }`
}
/>
</>
);
};

View File

@ -0,0 +1,61 @@
.wcpay-component-phone-number-input {
display: inline-block;
position: relative;
&__button {
position: absolute;
top: 0;
bottom: 0;
display: flex;
align-items: center;
padding-left: $gap-smaller;
padding-right: $gap-smallest;
background: none;
border: none;
&-arrow {
margin-top: 2px;
&.invert {
margin-top: 0;
transform: rotate(180deg);
}
}
}
&__input {
font-size: inherit;
}
&__menu {
position: absolute;
max-height: 200px;
min-width: 100%;
background-color: #fff;
border-radius: 2px;
border: 1px solid $gray-700;
margin: 1px 0;
z-index: 10000;
overflow-y: auto;
&[aria-hidden="true"] {
display: none;
}
&-item {
padding: $gap-smallest $gap-smaller;
margin: 0;
display: flex;
align-items: center;
&.highlighted {
background: #dcdcde; // $gray-5
}
}
}
&__flag {
width: 18px;
margin-right: $gap-smallest;
}
}

View File

@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PhoneNumberInput should match snapshot 1`] = `
<div>
<div
class="wcpay-component-phone-number-input"
>
<button
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="downshift-0-label downshift-0-toggle-button"
class="wcpay-component-phone-number-input__button"
id="downshift-0-toggle-button"
type="button"
>
<img
alt="US flag"
class="wcpay-component-phone-number-input__flag"
src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f1fa-1f1f8.png"
/>
+1
<span
class="wcpay-component-phone-number-input__button-arrow"
>
<svg
aria-hidden="true"
focusable="false"
height="18"
viewBox="0 0 24 24"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z"
/>
</svg>
</span>
</button>
<input
class="wcpay-component-phone-number-input__input"
style="padding-left: 0px;"
type="text"
value=""
/>
<ul
aria-hidden="true"
aria-labelledby="downshift-0-label"
class="wcpay-component-phone-number-input__menu"
id="downshift-0-menu"
role="listbox"
tabindex="-1"
/>
</div>
</div>
`;
exports[`PhoneNumberInput should match snapshot with custom renders 1`] = `
<div>
<div
class="wcpay-component-phone-number-input"
>
<button
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="downshift-1-label downshift-1-toggle-button"
class="wcpay-component-phone-number-input__button"
id="downshift-1-toggle-button"
type="button"
>
US
<span
class="wcpay-component-phone-number-input__button-arrow"
>
⬇️
</span>
</button>
<input
class="wcpay-component-phone-number-input__input"
style="padding-left: 0px;"
type="text"
value=""
/>
<ul
aria-hidden="true"
aria-labelledby="downshift-1-label"
class="wcpay-component-phone-number-input__menu"
id="downshift-1-menu"
role="listbox"
tabindex="-1"
/>
</div>
</div>
`;

View File

@ -0,0 +1,75 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { noop } from 'lodash';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import PhoneNumberInput from '..';
describe( 'PhoneNumberInput', () => {
it( 'should match snapshot', () => {
const { container } = render(
<PhoneNumberInput value="" onChange={ noop } />
);
expect( container ).toMatchSnapshot();
} );
it( 'should match snapshot with custom renders', () => {
const { container } = render(
<PhoneNumberInput
value=""
onChange={ noop }
selectedRender={ ( { name } ) => name }
itemRender={ ( { code } ) => code }
arrowRender={ () => '⬇️' }
/>
);
expect( container ).toMatchSnapshot();
} );
it( 'should render with provided `id`', () => {
render( <PhoneNumberInput id="test-id" value="" onChange={ noop } /> );
expect( screen.getByRole( 'textbox' ) ).toHaveAttribute(
'id',
'test-id'
);
} );
it( 'calls onChange callback on number input', () => {
const onChange = jest.fn();
render( <PhoneNumberInput value="" onChange={ onChange } /> );
const input = screen.getByRole( 'textbox' );
userEvent.type( input, '1' );
expect( onChange ).toHaveBeenCalledWith( '+1 1', '+11', 'US' );
} );
it( 'calls onChange callback when a country is selected', () => {
const onChange = jest.fn();
render( <PhoneNumberInput value="0 0" onChange={ onChange } /> );
const select = screen.getByRole( 'button' );
userEvent.click( select );
const option = screen.getByRole( 'option', { name: /es/i } );
userEvent.click( option );
expect( onChange ).toHaveBeenCalledWith( '+34 0 0', '+3400', 'ES' );
} );
it( 'prevents consecutive spaces and hyphens', () => {
const onChange = jest.fn();
render( <PhoneNumberInput value="0-" onChange={ onChange } /> );
const input = screen.getByRole( 'textbox' );
userEvent.type( input, '-' );
expect( onChange ).not.toHaveBeenCalled();
} );
} );

View File

@ -0,0 +1,105 @@
/**
* Internal dependencies
*/
import {
sanitizeNumber,
sanitizeInput,
numberToE164,
guessCountryKey,
decodeHtmlEntities,
countryToFlag,
parseData,
} from '../utils';
describe( 'PhoneNumberInput Utils', () => {
describe( 'sanitizeNumber', () => {
it( 'removes non-digit characters', () => {
const result = sanitizeNumber( '+1 23-45 67' );
expect( result ).toBe( '1234567' );
} );
} );
describe( 'sanitizeInput', () => {
it( 'removes non-digit characters except space and hyphen', () => {
const result = sanitizeInput( '+1 23--45 67 abc' );
expect( result ).toBe( '1 23--45 67 ' );
} );
} );
describe( 'numberToE164', () => {
it( 'converts a valid phone number to E.164 format', () => {
const result = numberToE164( '+1 23-45 67' );
expect( result ).toBe( '+1234567' );
} );
} );
describe( 'guessCountryKey', () => {
it( 'guesses the country code from a phone number', () => {
const countryCodes = {
'1': [ 'US' ],
'34': [ 'ES' ],
};
const result = guessCountryKey( '34666777888', countryCodes );
expect( result ).toBe( 'ES' );
} );
it( 'falls back to US if no match is found', () => {
const countryCodes = {
'34': [ 'ES' ],
};
const result = guessCountryKey( '1234567890', countryCodes );
expect( result ).toBe( 'US' );
} );
} );
describe( 'decodeHtmlEntities', () => {
it( 'replaces HTML entities from a predefined table', () => {
const result = decodeHtmlEntities(
'&atilde;&ccedil;&eacute;&iacute;'
);
expect( result ).toBe( 'ãçéí' );
} );
} );
describe( 'countryToFlag', () => {
it( 'converts a country code to a flag twemoji URL', () => {
const result = countryToFlag( 'US' );
expect( result ).toBe(
'https://s.w.org/images/core/emoji/14.0.0/72x72/1f1fa-1f1f8.png'
);
} );
} );
describe( 'parseData', () => {
it( 'parses the data into a more usable format', () => {
const data = {
AF: {
alpha2: 'AF',
code: '93',
priority: 0,
start: [ '7' ],
lengths: [ 9 ],
},
};
const { countries, countryCodes } = parseData( data );
expect( countries ).toEqual( {
AF: {
alpha2: 'AF',
code: '93',
flag: 'https://s.w.org/images/core/emoji/14.0.0/72x72/1f1e6-1f1eb.png',
lengths: [ 9 ],
name: 'AF',
priority: 0,
start: [ '7' ],
},
} );
expect( countryCodes ).toEqual( {
'93': [ 'AF' ],
'937': [ 'AF' ],
} );
} );
} );
} );

View File

@ -0,0 +1,30 @@
/**
* Internal dependencies
*/
import { validatePhoneNumber } from '../validation';
describe( 'PhoneNumberInput Validation', () => {
it( 'should return true for a valid US phone number', () => {
expect( validatePhoneNumber( '+12345678901', 'US' ) ).toBe( true );
} );
it( 'should return true for a valid phone number with country guessed from the number', () => {
expect( validatePhoneNumber( '+447123456789' ) ).toBe( true );
} );
it( 'should return false for a phone number with invalid format', () => {
expect( validatePhoneNumber( '1234567890', 'US' ) ).toBe( false );
} );
it( 'should return false for a phone number with incorrect country', () => {
expect( validatePhoneNumber( '+12345678901', 'GB' ) ).toBe( false );
} );
it( 'should return false for a phone number with incorrect length', () => {
expect( validatePhoneNumber( '+123456', 'US' ) ).toBe( false );
} );
it( 'should return false for a phone number with incorrect start', () => {
expect( validatePhoneNumber( '+11234567890', 'US' ) ).toBe( false );
} );
} );

View File

@ -0,0 +1,10 @@
export type DataType = Record<
string,
{
alpha2: string;
code: string;
priority: number;
start?: string[];
lengths?: number[];
}
>;

View File

@ -0,0 +1,133 @@
/**
* Internal dependencies
*/
import type { DataType } from './types';
const mapValues = < T, U >(
object: Record< string, T >,
iteratee: ( value: T ) => U
): Record< string, U > => {
const result: Record< string, U > = {};
for ( const key in object ) {
result[ key ] = iteratee( object[ key ] );
}
return result;
};
/**
* Removes any non-digit character.
*/
export const sanitizeNumber = ( number: string ): string =>
number.replace( /\D/g, '' );
/**
* Removes any non-digit character, except space and hyphen.
*/
export const sanitizeInput = ( number: string ): string =>
number.replace( /[^\d -]/g, '' );
/**
* Converts a valid phone number to E.164 format.
*/
export const numberToE164 = ( number: string ): string =>
`+${ sanitizeNumber( number ) }`;
/**
* Guesses the country code from a phone number.
* If no match is found, it will fallback to US.
*
* @param number Phone number including country code.
* @param countryCodes List of country codes.
* @return Country code in ISO 3166-1 alpha-2 format. e.g. US
*/
export const guessCountryKey = (
number: string,
countryCodes: Record< string, string[] >
): string => {
number = sanitizeNumber( number );
// Match each digit against countryCodes until a match is found
for ( let i = number.length; i > 0; i-- ) {
const match = countryCodes[ number.substring( 0, i ) ];
if ( match ) return match[ 0 ];
}
return 'US';
};
const entityTable: Record< string, string > = {
atilde: 'ã',
ccedil: 'ç',
eacute: 'é',
iacute: 'í',
};
/**
* Replaces HTML entities from a predefined table.
*/
export const decodeHtmlEntities = ( str: string ): string =>
str.replace( /&(\S+?);/g, ( match, p1 ) => entityTable[ p1 ] || match );
const countryNames: Record< string, string > = mapValues(
{
AC: 'Ascension Island',
XK: 'Kosovo',
...( window.wcSettings?.countries || [] ),
},
( name ) => decodeHtmlEntities( name )
);
/**
* Converts a country code to a flag twemoji URL from `s.w.org`.
*
* @param alpha2 Country code in ISO 3166-1 alpha-2 format. e.g. US
* @return Country flag emoji URL.
*/
export const countryToFlag = ( alpha2: string ): string => {
const name = alpha2
.split( '' )
.map( ( char ) =>
( 0x1f1e5 + ( char.charCodeAt( 0 ) % 32 ) ).toString( 16 )
)
.join( '-' );
return `https://s.w.org/images/core/emoji/14.0.0/72x72/${ name }.png`;
};
const pushOrAdd = (
acc: Record< string, string[] >,
key: string,
value: string
) => {
if ( acc[ key ] ) {
if ( ! acc[ key ].includes( value ) ) acc[ key ].push( value );
} else {
acc[ key ] = [ value ];
}
};
/**
* Parses the data from `data.ts` into a more usable format.
*/
export const parseData = ( data: DataType ) => ( {
countries: mapValues( data, ( country ) => ( {
...country,
name: countryNames[ country.alpha2 ] ?? country.alpha2,
flag: countryToFlag( country.alpha2 ),
} ) ),
countryCodes: Object.values( data )
.sort( ( a, b ) => ( a.priority > b.priority ? 1 : -1 ) )
.reduce( ( acc, { code, alpha2, start } ) => {
pushOrAdd( acc, code, alpha2 );
if ( start ) {
for ( const str of start ) {
for ( let i = 1; i <= str.length; i++ ) {
pushOrAdd( acc, code + str.substring( 0, i ), alpha2 );
}
}
}
return acc;
}, {} as Record< string, string[] > ),
} );
export type Country = ReturnType< typeof parseData >[ 'countries' ][ 0 ];

View File

@ -0,0 +1,53 @@
/**
* Internal dependencies
*/
import data from './data';
import { guessCountryKey, parseData } from './utils';
const { countries, countryCodes } = parseData( data );
/**
* Mobile phone number validation based on `data.ts` rules.
* If no country is provided, it will try to guess it from the number or fallback to US.
*
* @param number Phone number to validate in E.164 format. e.g. +12345678901
* @param countryAlpha2 Country code in ISO 3166-1 alpha-2 format. e.g. US
* @return boolean
*/
export const validatePhoneNumber = (
number: string,
countryAlpha2?: string
): boolean => {
// Sanitize number.
number = '+' + number.replace( /\D/g, '' );
// Return early If format is not E.164.
if ( ! /^\+[1-9]\d{1,14}$/.test( number ) ) {
return false;
}
// If country is not provided, try to guess it from the number or fallback to US.
if ( ! countryAlpha2 ) {
countryAlpha2 = guessCountryKey( number, countryCodes );
}
const country = countries[ countryAlpha2 ];
// Remove `+` and country code.
number = number.slice( country.code.length + 1 );
// If country as `lengths` defined check if number matches.
if ( country.lengths && ! country.lengths.includes( number.length ) ) {
return false;
}
// If country has `start` defined check if number starts with one of them.
if (
country.start &&
! country.start.some( ( prefix ) => number.startsWith( prefix ) )
) {
return false;
}
return true;
};

View File

@ -59,4 +59,5 @@
@import 'experimental-select-tree-control/select-tree.scss';
@import 'product-section-layout/style.scss';
@import 'tree-select-control/index.scss';
@import 'progress-bar/style.scss';
@import 'progress-bar/style.scss';
@import 'phone-number-input/style.scss';

View File

@ -2,6 +2,7 @@ declare global {
interface Window {
wcSettings: {
variationTitleAttributesSeparator?: string;
countries: Record< string, string >;
};
}
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add name and parent_id to the ProductVariation type definition

View File

@ -55,7 +55,7 @@ export interface ProductVariationImage {
export type ProductVariation = Omit<
Product,
'name' | 'slug' | 'attributes' | 'images' | 'manage_stock'
'slug' | 'attributes' | 'images' | 'manage_stock'
> & {
attributes: ProductVariationAttribute[];
/**
@ -70,6 +70,10 @@ export type ProductVariation = Omit<
* @default false
*/
manage_stock: boolean | 'parent';
/**
* The product id this variation belongs to
*/
parent_id: number;
};
type Query = Omit< ProductQuery, 'name' >;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Added shouldLoop prop for the Loader component to determine if looping should happen

View File

@ -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 }</>;

View File

@ -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',
},

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add download file list product block

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Set shipping disabled when the product is virtual

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add virtual and downloads related controls to variation management quick actions

View File

@ -1,4 +1,4 @@
Significance: minor
Type: add
Add ProgressBar component
Add new file dropdown menu

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new VariationSwitcherFooter component for switching variations on edit variation page.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Export RemoveConfirmationModal to be used outside of the product editor package

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add edit button to each variation to redirect to the single variation page

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add support to downloads block to use the context postType

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add 'min' and 'max' attributes to number block

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Change the blocks editor header to support variations #40606

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Display notice at the top single variations #40679

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add unregister function to the validation provider and trigger this from the useValidation hook.

Some files were not shown because too many files have changed in this diff Show More