From ef023f75c6685e5ec5ffa7646baf726b438558e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20L=C3=B3pez?= Date: Mon, 4 Sep 2023 15:07:32 +0200 Subject: [PATCH 01/59] Add instructions for testing contributions in the CONTRIBUTING.md file. --- .github/CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 63385a6a00f..e8d07fdc781 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -37,6 +37,10 @@ Please take a moment to review the [project readme](https://github.com/woocommer - Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more. - Before pushing commits to GitHub, check your code against our code standards. For PHP code in the WooCommerce Core project you can do this by running `pnpm --filter=woocommerce run lint:php:changes:branch`. - Whenever possible, please fix pre-existing code standards errors in code that you change. +- Please consider adding appropriate tests related to your change if applicabble such as unit, API and E2E. You can check the following guides for this purpose: + - [Writing unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md#writing-tests). + - [Writing API tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests#guide-for-writing-tests). + - [Writing E2E tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#guide-for-writing-e2e-tests). - Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured. - When committing, reference your issue number (#1234) and include a note about the fix. - Ensure that your code supports the minimum supported versions of PHP and WordPress; this is shown at the top of the `readme.txt` file. From 30e2437d43e225ebc69727ece9742c666b6fea9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20L=C3=B3pez?= Date: Mon, 4 Sep 2023 15:12:02 +0200 Subject: [PATCH 02/59] Fix typo. --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e8d07fdc781..b2f1b02d04a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -37,7 +37,7 @@ Please take a moment to review the [project readme](https://github.com/woocommer - Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more. - Before pushing commits to GitHub, check your code against our code standards. For PHP code in the WooCommerce Core project you can do this by running `pnpm --filter=woocommerce run lint:php:changes:branch`. - Whenever possible, please fix pre-existing code standards errors in code that you change. -- Please consider adding appropriate tests related to your change if applicabble such as unit, API and E2E. You can check the following guides for this purpose: +- Please consider adding appropriate tests related to your change if applicable such as unit, API and E2E. You can check the following guides for this purpose: - [Writing unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md#writing-tests). - [Writing API tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests#guide-for-writing-tests). - [Writing E2E tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#guide-for-writing-e2e-tests). From 1f3a0962ff4ac7ac1ea005a501be65b369b53879 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 14:37:18 -0700 Subject: [PATCH 03/59] Uninstall and reinstall WC --- .../tests/api-core-tests/global-setup.js | 91 ++++++++++++++++++- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index 60a0a4bb801..2b8b89dd97e 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -1,8 +1,91 @@ -const { site } = require( './utils' ); +const { chromium, expect } = require( '@playwright/test' ); module.exports = async ( config ) => { - // If BASE_URL is configured, we can assume we're on CI - if ( process.env.API_BASE_URL ) { - await site.reset( process.env.USER_KEY, process.env.USER_SECRET ); + const { baseURL, userAgent } = config.projects[ 0 ].use; + const contextOptions = { baseURL, userAgent }; + + const browser = await chromium.launch(); + const setupContext = await browser.newContext( contextOptions ); + const setupPage = await setupContext.newPage(); + + // If API_BASE_URL is configured and doesn't include localhost, running on daily + if ( + process.env.API_BASE_URL && + ! process.env.API_BASE_URL.includes( 'localhost' ) + ) { + let adminLoggedIn = false; + + console.log( '--------------------------------------' ); + console.log( 'Running daily tests, resetting site...' ); + console.log( '--------------------------------------' ); + + const adminRetries = 5; + for ( let i = 0; i < adminRetries; i++ ) { + try { + console.log( 'Trying to log-in as admin...' ); + await setupPage.goto( '/wp-admin' ); + await setupPage + .locator( 'input[name="log"]' ) + .fill( process.env.USER_KEY ); + await setupPage + .locator( 'input[name="pwd"]' ) + .fill( process.env.USER_SECRET ); + await setupPage.locator( 'text=Log In' ).click(); + + await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText( + 'Dashboard' + ); + console.log( 'Logged-in as admin successfully.' ); + adminLoggedIn = true; + break; + } catch ( e ) { + console.log( + `Admin log-in failed, Retrying... ${ i }/${ adminRetries }` + ); + console.log( e ); + } + } + + if ( ! adminLoggedIn ) { + console.error( + 'Cannot proceed api test, as admin login failed. Please check if the test site has been setup correctly.' + ); + process.exit( 1 ); + } + + await setupPage.goto( 'wp-admin/plugins.php' ); + await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText( + 'Plugins' + ); + + console.log( 'Deactivating WooCommerce Plugin...' ); + await setupPage.locator( '#deactivate-woocommerce' ).click(); + await expect( setupPage.locator( 'div#message' ) ).toHaveText( + 'Plugin deactivated.Dismiss this notice.' + ); + + console.log( 'Deleting WooCommerce Plugin...' ); + setupPage.on( 'dialog', ( dialog ) => dialog.accept() ); + await setupPage.locator( '#delete-woocommerce' ).click(); + await expect( setupPage.locator( '#woocommerce-deleted' ) ).toHaveText( + 'WooCommerce was successfully deleted.' + ); + + console.log( 'Reinstalling WooCommerce Plugin...' ); + await setupPage.goto( 'wp-admin/plugin-install.php' ); + await setupPage.locator( '#search-plugins' ).type( 'woocommerce' ); + await setupPage + .getByRole( 'link', { + name: /Install WooCommerce \d+\.\d+\.\d+ now/g, + } ) + .click(); + await setupPage.getByRole( 'link', { name: 'Activate' } ).click(); + + console.log( 'WooCommerce Re-installed.' ); + await expect( + setupPage.getByRole( 'heading', { name: 'Welcome to Woo!' } ) + ).toBeVisible(); + + // await site.reset( process.env.USER_KEY, process.env.USER_SECRET ); } }; From 5e222b6bdcfde0ae66744140e173d6288ee03a5e Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 14:39:34 -0700 Subject: [PATCH 04/59] Remove soft assertion --- .../tests/api-core-tests/tests/shipping/shipping-zones.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js index 6eb02ed326f..a984099972f 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js @@ -171,7 +171,7 @@ test.describe( 'Shipping zones API tests', () => { const putResponseStateOnlyJSON = await putResponseStateOnly.json(); expect( putResponseStateOnly.status() ).toEqual( 200 ); - expect.soft( putResponseStateOnlyJSON ).toHaveLength(0); + expect( putResponseStateOnlyJSON ).toHaveLength(0); } ); From e316d8745de6f4ae96d28fb20dcbe58e7ab69efc Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 15:02:05 -0700 Subject: [PATCH 05/59] Add changelog --- plugins/woocommerce/changelog/api-full-site-reset-on-daily | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/api-full-site-reset-on-daily diff --git a/plugins/woocommerce/changelog/api-full-site-reset-on-daily b/plugins/woocommerce/changelog/api-full-site-reset-on-daily new file mode 100644 index 00000000000..4a677c8cb22 --- /dev/null +++ b/plugins/woocommerce/changelog/api-full-site-reset-on-daily @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Run a full reset on API daily test site From b5e5f33825d74098f349fea8617b0950e4906c0c Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 16:06:49 -0700 Subject: [PATCH 06/59] Update to nightly release after reset --- .github/workflows/smoke-test-daily.yml | 6 - .../tests/api-core-tests/global-setup.js | 75 ++++- .../api-core-tests/utils/plugin-utils.js | 260 ++++++++++++++++++ 3 files changed, 329 insertions(+), 12 deletions(-) create mode 100644 plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index c2febda6161..2b8739d655b 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -45,12 +45,6 @@ jobs: working-directory: plugins/woocommerce run: pnpm exec playwright install chromium - - name: Run 'Update WooCommerce' test. - working-directory: plugins/woocommerce - env: - UPDATE_WC: nightly - run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js - - name: Run API tests. working-directory: plugins/woocommerce env: diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index 2b8b89dd97e..efd4fbf0d2a 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -1,4 +1,46 @@ const { chromium, expect } = require( '@playwright/test' ); +const { GITHUB_TOKEN, UPDATE_WC } = process.env; +const { downloadZip, deleteZip } = require( './utils/plugin-utils' ); +const axios = require( 'axios' ).default; + +const getWCDownloadURL = async () => { + const requestConfig = { + method: 'get', + url: 'https://api.github.com/repos/woocommerce/woocommerce/releases', + headers: { + Accept: 'application/vnd.github+json', + }, + params: { + per_page: 100, + }, + }; + if ( GITHUB_TOKEN ) { + requestConfig.headers.Authorization = `Bearer ${ GITHUB_TOKEN }`; + } + const response = await axios( requestConfig ).catch( ( error ) => { + if ( error.response ) { + console.log( error.response.data ); + } + throw new Error( error.message ); + } ); + const releaseWithTagName = response.data.find( + ( { tag_name } ) => tag_name === UPDATE_WC + ); + if ( ! releaseWithTagName ) { + throw new Error( + `No release with tag_name="${ UPDATE_WC }" found. If "${ UPDATE_WC }" is a draft release, make sure to specify a GITHUB_TOKEN environment variable.` + ); + } + const wcZipAsset = releaseWithTagName.assets.find( ( { name } ) => + name.match( /^woocommerce(-trunk-nightly)?\.zip$/ ) + ); + if ( wcZipAsset ) { + return GITHUB_TOKEN ? wcZipAsset.url : wcZipAsset.browser_download_url; + } + throw new Error( + `WooCommerce release with tag "${ UPDATE_WC }" found, but does not have a WooCommerce ZIP asset.` + ); +}; module.exports = async ( config ) => { const { baseURL, userAgent } = config.projects[ 0 ].use; @@ -8,7 +50,16 @@ module.exports = async ( config ) => { const setupContext = await browser.newContext( contextOptions ); const setupPage = await setupContext.newPage(); - // If API_BASE_URL is configured and doesn't include localhost, running on daily + const url = await getWCDownloadURL(); + const params = { url }; + + if ( GITHUB_TOKEN ) { + params.authorizationToken = GITHUB_TOKEN; + } + + const woocommerceZipPath = await downloadZip( params ); + + // If API_BASE_URL is configured and doesn't include localhost, running on daily host if ( process.env.API_BASE_URL && ! process.env.API_BASE_URL.includes( 'localhost' ) @@ -73,19 +124,31 @@ module.exports = async ( config ) => { console.log( 'Reinstalling WooCommerce Plugin...' ); await setupPage.goto( 'wp-admin/plugin-install.php' ); - await setupPage.locator( '#search-plugins' ).type( 'woocommerce' ); + await setupPage.locator( 'a.upload-view-toggle' ).click(); + await expect( setupPage.locator( 'p.install-help' ) ).toBeVisible(); + await expect( setupPage.locator( 'p.install-help' ) ).toContainText( + 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' + ); + const [ fileChooser ] = await Promise.all( [ + setupPage.waitForEvent( 'filechooser' ), + setupPage.locator( '#pluginzip' ).click(), + ] ); + await fileChooser.setFiles( woocommerceZipPath ); await setupPage - .getByRole( 'link', { - name: /Install WooCommerce \d+\.\d+\.\d+ now/g, - } ) + .locator( '#install-plugin-submit' ) + .click( { timeout: 60000 } ); + await setupPage.waitForLoadState( 'networkidle' ); + await setupPage + .getByRole( 'link', { name: 'Activate Plugin' } ) .click(); - await setupPage.getByRole( 'link', { name: 'Activate' } ).click(); console.log( 'WooCommerce Re-installed.' ); await expect( setupPage.getByRole( 'heading', { name: 'Welcome to Woo!' } ) ).toBeVisible(); + await deleteZip( woocommerceZipPath ); + // await site.reset( process.env.USER_KEY, process.env.USER_SECRET ); } }; diff --git a/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js b/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js new file mode 100644 index 00000000000..a3be326a9a5 --- /dev/null +++ b/plugins/woocommerce/tests/api-core-tests/utils/plugin-utils.js @@ -0,0 +1,260 @@ +const { APIRequest } = require( '@playwright/test' ); +const axios = require( 'axios' ).default; +const fs = require( 'fs' ); +const path = require( 'path' ); +const { promisify } = require( 'util' ); +const execAsync = promisify( require( 'child_process' ).exec ); + +/** + * Encode basic auth username and password to be used in HTTP Authorization header. + * + * @param {string} username + * @param {string} password + * @returns Base64-encoded string + */ +const encodeCredentials = ( username, password ) => { + return Buffer.from( `${ username }:${ password }` ).toString( 'base64' ); +}; + +/** + * Deactivate and delete a plugin specified by the given `slug` using the WordPress API. + * + * @param {object} params + * @param {APIRequest} params.request + * @param {string} params.baseURL + * @param {string} params.slug + * @param {string} params.username + * @param {string} params.password + */ +export const deletePlugin = async ( { + request, + baseURL, + slug, + username, + password, +} ) => { + // Check if plugin is installed by getting the list of installed plugins, and then finding the one whose `textdomain` property equals `slug`. + const apiContext = await request.newContext( { + baseURL, + extraHTTPHeaders: { + Authorization: `Basic ${ encodeCredentials( username, password ) }`, + cookie: '', + }, + } ); + const listPluginsResponse = await apiContext.get( + `/wp-json/wp/v2/plugins`, + { + failOnStatusCode: true, + } + ); + const pluginsList = await listPluginsResponse.json(); + const pluginToDelete = pluginsList.find( + ( { textdomain } ) => textdomain === slug + ); + + // If installed, get its `plugin` value and use it to deactivate and delete it. + if ( pluginToDelete ) { + const { plugin } = pluginToDelete; + const requestURL = `/wp-json/wp/v2/plugins/${ plugin }`; + + await apiContext.put( requestURL, { + data: { status: 'inactive' }, + } ); + + await apiContext.delete( requestURL ); + } +}; + +/** + * Download the zip file from a remote location. + * + * @param {object} param + * @param {string} param.url + * @param {string} param.repository + * @param {string} param.authorizationToken + * @param {boolean} param.prerelease + * @param {string} param.downloadDir + * + * @param {string} url The URL where the zip file is located. Takes precedence over `repository`. + * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. Ignored when `url` was given. + * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required. + * @param {boolean} prerelease Flag on whether to get a prelease or not. Default `false`. + * @param {string} downloadDir Relative path to the download directory. Non-existing folders will be auto-created. Defaults to `tmp` under current working directory. + * + * @return {string} Absolute path to the downloaded zip. + */ +export const downloadZip = async ( { + url, + repository, + authorizationToken, + prerelease = false, + downloadDir = 'tmp', +} ) => { + let zipFilename = path.basename( url || repository ); + zipFilename = zipFilename.endsWith( '.zip' ) + ? zipFilename + : zipFilename.concat( '.zip' ); + const zipFilePath = path.resolve( downloadDir, zipFilename ); + + let response; + + // Create destination folder. + fs.mkdirSync( downloadDir, { recursive: true } ); + + const downloadURL = + url ?? + ( await getLatestReleaseZipUrl( { + repository, + authorizationToken, + prerelease, + } ) ); + + // Download the zip. + const options = { + method: 'get', + url: downloadURL, + responseType: 'stream', + headers: { + Authorization: authorizationToken + ? `token ${ authorizationToken }` + : '', + Accept: 'application/octet-stream', + }, + }; + + response = await axios( options ).catch( ( error ) => { + if ( error.response ) { + console.error( error.response.data ); + } + throw new Error( error.message ); + } ); + + response.data.pipe( fs.createWriteStream( zipFilePath ) ); + + return zipFilePath; +}; + +/** + * Delete a zip file. Useful when cleaning up downloaded plugin zips. + * + * @param {string} zipFilePath Local file path to the ZIP. + */ +export const deleteZip = async ( zipFilePath ) => { + await fs.unlink( zipFilePath, ( err ) => { + if ( err ) throw err; + } ); +}; + +/** + * Get the download URL of the latest release zip for a plugin using GitHub API. + * + * @param {{repository: string, authorizationToken: string, prerelease: boolean, perPage: number}} param + * @param {string} repository The repository owner and name. For example: `woocommerce/woocommerce`. + * @param {string} authorizationToken Authorization token used to authenticate with the GitHub API if required. + * @param {boolean} prerelease Flag on whether to get a prelease or not. + * @param {number} perPage Limit of entries returned from the latest releases list, defaults to 3. + * @return {string} Download URL for the release zip file. + */ +export const getLatestReleaseZipUrl = async ( { + repository, + authorizationToken, + prerelease = false, + perPage = 3, +} ) => { + let release; + + const requesturl = prerelease + ? `https://api.github.com/repos/${ repository }/releases?per_page=${ perPage }` + : `https://api.github.com/repos/${ repository }/releases/latest`; + + const options = { + method: 'get', + url: requesturl, + headers: { + Authorization: authorizationToken + ? `token ${ authorizationToken }` + : '', + }, + }; + + // Get the first prerelease, or the latest release. + let response; + try { + response = await axios( options ); + } catch ( error ) { + let errorMessage = + 'Something went wrong when downloading the plugin.\n'; + + if ( error.response ) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + errorMessage = errorMessage.concat( + `Response status: ${ error.response.status } ${ error.response.statusText }`, + '\n', + `Response body:`, + '\n', + JSON.stringify( error.response.data, null, 2 ), + '\n' + ); + } else if ( error.request ) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + errorMessage = errorMessage.concat( + JSON.stringify( error.request, null, 2 ), + '\n' + ); + } else { + // Something happened in setting up the request that triggered an Error + errorMessage = errorMessage.concat( error.toJSON(), '\n' ); + } + + throw new Error( errorMessage ); + } + + release = prerelease + ? response.data.find( ( { prerelease } ) => prerelease ) + : response.data; + + // If response contains assets, return URL of first asset. + // Otherwise, return the github.com URL from the tag name. + const { assets } = release; + if ( assets && assets.length ) { + return assets[ 0 ].url; + } else { + const tagName = release.tag_name; + return `https://github.com/${ repository }/archive/${ tagName }.zip`; + } +}; + +/** + * Install a plugin using WP CLI within a WP ENV environment. + * This is a workaround to the "The uploaded file exceeds the upload_max_filesize directive in php.ini" error encountered when uploading a plugin to the local WP Env E2E environment through the UI. + * + * @see https://github.com/WordPress/gutenberg/issues/29430 + * + * @param {string} pluginPath + */ +export const installPluginThruWpCli = async ( pluginPath ) => { + const runWpCliCommand = async ( command ) => { + const { stdout, stderr } = await execAsync( + `pnpm exec wp-env run tests-cli -- ${ command }` + ); + + console.log( stdout ); + console.error( stderr ); + }; + + const wpEnvPluginPath = pluginPath.replace( + /.*\/plugins\/woocommerce/, + 'wp-content/plugins/woocommerce' + ); + + await runWpCliCommand( `ls ${ wpEnvPluginPath }` ); + + await runWpCliCommand( + `wp plugin install --activate --force ${ wpEnvPluginPath }` + ); + + await runWpCliCommand( `wp plugin list` ); +}; From 575e2346af6e5c16000b05a2dc229250be64a13c Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 16:14:51 -0700 Subject: [PATCH 07/59] Set env variable to update to nightly --- .github/workflows/smoke-test-daily.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/smoke-test-daily.yml b/.github/workflows/smoke-test-daily.yml index 2b8739d655b..96c8b5317f1 100644 --- a/.github/workflows/smoke-test-daily.yml +++ b/.github/workflows/smoke-test-daily.yml @@ -32,6 +32,7 @@ jobs: CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }} CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }} DEFAULT_TIMEOUT_OVERRIDE: 120000 + UPDATE_WC: 'nightly' steps: - uses: actions/checkout@v3 From c497b37a9617bc6c5477cb4591554e1b7287e591 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Wed, 6 Sep 2023 16:31:55 -0700 Subject: [PATCH 08/59] Tweak so setup will skip locally --- .../tests/api-core-tests/global-setup.js | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index efd4fbf0d2a..e8400a00fea 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -3,45 +3,6 @@ const { GITHUB_TOKEN, UPDATE_WC } = process.env; const { downloadZip, deleteZip } = require( './utils/plugin-utils' ); const axios = require( 'axios' ).default; -const getWCDownloadURL = async () => { - const requestConfig = { - method: 'get', - url: 'https://api.github.com/repos/woocommerce/woocommerce/releases', - headers: { - Accept: 'application/vnd.github+json', - }, - params: { - per_page: 100, - }, - }; - if ( GITHUB_TOKEN ) { - requestConfig.headers.Authorization = `Bearer ${ GITHUB_TOKEN }`; - } - const response = await axios( requestConfig ).catch( ( error ) => { - if ( error.response ) { - console.log( error.response.data ); - } - throw new Error( error.message ); - } ); - const releaseWithTagName = response.data.find( - ( { tag_name } ) => tag_name === UPDATE_WC - ); - if ( ! releaseWithTagName ) { - throw new Error( - `No release with tag_name="${ UPDATE_WC }" found. If "${ UPDATE_WC }" is a draft release, make sure to specify a GITHUB_TOKEN environment variable.` - ); - } - const wcZipAsset = releaseWithTagName.assets.find( ( { name } ) => - name.match( /^woocommerce(-trunk-nightly)?\.zip$/ ) - ); - if ( wcZipAsset ) { - return GITHUB_TOKEN ? wcZipAsset.url : wcZipAsset.browser_download_url; - } - throw new Error( - `WooCommerce release with tag "${ UPDATE_WC }" found, but does not have a WooCommerce ZIP asset.` - ); -}; - module.exports = async ( config ) => { const { baseURL, userAgent } = config.projects[ 0 ].use; const contextOptions = { baseURL, userAgent }; @@ -50,20 +11,61 @@ module.exports = async ( config ) => { const setupContext = await browser.newContext( contextOptions ); const setupPage = await setupContext.newPage(); - const url = await getWCDownloadURL(); - const params = { url }; - - if ( GITHUB_TOKEN ) { - params.authorizationToken = GITHUB_TOKEN; - } - - const woocommerceZipPath = await downloadZip( params ); - // If API_BASE_URL is configured and doesn't include localhost, running on daily host if ( process.env.API_BASE_URL && ! process.env.API_BASE_URL.includes( 'localhost' ) ) { + const getWCDownloadURL = async () => { + const requestConfig = { + method: 'get', + url: 'https://api.github.com/repos/woocommerce/woocommerce/releases', + headers: { + Accept: 'application/vnd.github+json', + }, + params: { + per_page: 100, + }, + }; + if ( GITHUB_TOKEN ) { + requestConfig.headers.Authorization = `Bearer ${ GITHUB_TOKEN }`; + } + const response = await axios( requestConfig ).catch( ( error ) => { + if ( error.response ) { + console.log( error.response.data ); + } + throw new Error( error.message ); + } ); + const releaseWithTagName = response.data.find( + ( { tag_name } ) => tag_name === UPDATE_WC + ); + if ( ! releaseWithTagName ) { + throw new Error( + `No release with tag_name="${ UPDATE_WC }" found. If "${ UPDATE_WC }" is a draft release, make sure to specify a GITHUB_TOKEN environment variable.` + ); + } + const wcZipAsset = releaseWithTagName.assets.find( ( { name } ) => + name.match( /^woocommerce(-trunk-nightly)?\.zip$/ ) + ); + if ( wcZipAsset ) { + return GITHUB_TOKEN + ? wcZipAsset.url + : wcZipAsset.browser_download_url; + } + throw new Error( + `WooCommerce release with tag "${ UPDATE_WC }" found, but does not have a WooCommerce ZIP asset.` + ); + }; + + const url = await getWCDownloadURL(); + const params = { url }; + + if ( GITHUB_TOKEN ) { + params.authorizationToken = GITHUB_TOKEN; + } + + const woocommerceZipPath = await downloadZip( params ); + let adminLoggedIn = false; console.log( '--------------------------------------' ); From 6dc7b83c9c4a3fe32bd0996f2f8ebd46df4ae420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20L=C3=B3pez?= Date: Thu, 7 Sep 2023 13:56:38 +0200 Subject: [PATCH 09/59] Fix documentation based on feedback from PR review. --- .github/CONTRIBUTING.md | 6 +++--- plugins/woocommerce/tests/README.md | 8 ++++---- plugins/woocommerce/tests/api-core-tests/README.md | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b2f1b02d04a..b603f2378b8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -37,9 +37,9 @@ Please take a moment to review the [project readme](https://github.com/woocommer - Run our build process described in the document on [how to set up WooCommerce development environment](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment), it will install our pre-commit hook, code sniffs, dependencies, and more. - Before pushing commits to GitHub, check your code against our code standards. For PHP code in the WooCommerce Core project you can do this by running `pnpm --filter=woocommerce run lint:php:changes:branch`. - Whenever possible, please fix pre-existing code standards errors in code that you change. -- Please consider adding appropriate tests related to your change if applicable such as unit, API and E2E. You can check the following guides for this purpose: - - [Writing unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md#writing-tests). - - [Writing API tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests#guide-for-writing-tests). +- Please consider adding appropriate tests related to your change if applicable such as unit, API and E2E tests. You can check the following guides for this purpose: + - [Writing unit tests](https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/tests/README.md#guide-for-writing-unit-tests). + - [Writing API tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests#guide-for-writing-api-tests). - [Writing E2E tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/e2e-pw#guide-for-writing-e2e-tests). - Ensure you use LF line endings in your code editor. Use [EditorConfig](http://editorconfig.org/) if your editor supports it so that indentation, line endings and other settings are auto configured. - When committing, reference your issue number (#1234) and include a note about the fix. diff --git a/plugins/woocommerce/tests/README.md b/plugins/woocommerce/tests/README.md index 7d931f510e9..3390af5ac24 100644 --- a/plugins/woocommerce/tests/README.md +++ b/plugins/woocommerce/tests/README.md @@ -9,10 +9,10 @@ This document discusses unit tests. See [the e2e README](https://github.com/wooc - [Initial Setup](#initial-setup) - [MySQL database](#mysql-database) - [Setup instructions](#setup-instructions) - - [Running Tests](#running-tests) + - [Running Unit Tests](#running-unit-tests) - [Troubleshooting](#troubleshooting) - [Running tests in PHP 8](#running-tests-in-php-8) - - [Writing Tests](#writing-tests) + - [Guide for Writing Unit Tests](#guide-for-writing-unit-tests) - [Automated Tests](#automated-tests) - [Code Coverage](#code-coverage) @@ -51,7 +51,7 @@ Example: **Important**: The `` database will be created if it doesn't exist and all data will be removed during testing. -## Running Tests +## Running Unit Tests Change to the plugin root directory and type: @@ -97,7 +97,7 @@ $ tests/bin/install.sh woocommerce_tests_1 root root Note that `woocommerce_tests` changed to `woocommerce_tests_1` as the `woocommerce_tests` database already exists due to the prior command. -## Writing Tests +## Guide for Writing Unit Tests There are three different unit test directories: diff --git a/plugins/woocommerce/tests/api-core-tests/README.md b/plugins/woocommerce/tests/api-core-tests/README.md index db6e49ef917..747e672b086 100644 --- a/plugins/woocommerce/tests/api-core-tests/README.md +++ b/plugins/woocommerce/tests/api-core-tests/README.md @@ -8,7 +8,7 @@ This package contains automated API tests for WooCommerce, based on Playwright a - [Introduction](#introduction) - [About the Environment](#about-the-environment) - [Test Variables](#test-variables) -- [Guide for writing tests](#guide-for-writing-tests) +- [Guide for writing API tests](#guide-for-writing-api-tests) - [What aspects of the API should we test?](#what-aspects-of-the-api-should-we-test) - [Creating test structure](#creating-test-structure) - [Test Data Setup/Teardown](#test-data-setupteardown) @@ -130,7 +130,7 @@ After you run a test, it's best to restart the environment to start from a fresh - `pnpm env:destroy` when you make changes to `.wp-env.json` - `pnpm env:test` to spin up the test environment -## Guide for writing tests +## Guide for writing API tests When writing new tests, a good source on how to get started is to reference the [existing tests](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/tests). Data that is required for the tests should be located in an equivalent file in the [data](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce/tests/api-core-tests/data) folder. From dcd05a7be3821ec58c1fccb3e06332709b878d11 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 7 Sep 2023 12:01:18 +0000 Subject: [PATCH 10/59] Add changefile(s) from automation for the following project(s): woocommerce --- .../changelog/add-instructions-for-testing-contribution | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-instructions-for-testing-contribution diff --git a/plugins/woocommerce/changelog/add-instructions-for-testing-contribution b/plugins/woocommerce/changelog/add-instructions-for-testing-contribution new file mode 100644 index 00000000000..630ac6debb2 --- /dev/null +++ b/plugins/woocommerce/changelog/add-instructions-for-testing-contribution @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Update CONTRIBUTING.md file to include instructions on how adding unit, API and E2E tests when applicable. + + From 7011a848589f13068a417b8300b5d6570cdaf7c8 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Thu, 7 Sep 2023 13:13:16 -0700 Subject: [PATCH 11/59] Retry plugin installation if failed --- .../tests/api-core-tests/global-setup.js | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index e8400a00fea..85bb1f91fdb 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -67,6 +67,7 @@ module.exports = async ( config ) => { const woocommerceZipPath = await downloadZip( params ); let adminLoggedIn = false; + let pluginActive = false; console.log( '--------------------------------------' ); console.log( 'Running daily tests, resetting site...' ); @@ -124,25 +125,51 @@ module.exports = async ( config ) => { 'WooCommerce was successfully deleted.' ); - console.log( 'Reinstalling WooCommerce Plugin...' ); - await setupPage.goto( 'wp-admin/plugin-install.php' ); - await setupPage.locator( 'a.upload-view-toggle' ).click(); - await expect( setupPage.locator( 'p.install-help' ) ).toBeVisible(); - await expect( setupPage.locator( 'p.install-help' ) ).toContainText( - 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' - ); - const [ fileChooser ] = await Promise.all( [ - setupPage.waitForEvent( 'filechooser' ), - setupPage.locator( '#pluginzip' ).click(), - ] ); - await fileChooser.setFiles( woocommerceZipPath ); - await setupPage - .locator( '#install-plugin-submit' ) - .click( { timeout: 60000 } ); - await setupPage.waitForLoadState( 'networkidle' ); - await setupPage - .getByRole( 'link', { name: 'Activate Plugin' } ) - .click(); + for ( let i = 0; i < adminRetries; i++ ) { + try { + console.log( 'Reinstalling WooCommerce Plugin...' ); + await setupPage.goto( 'wp-admin/plugin-install.php' ); + await setupPage.locator( 'a.upload-view-toggle' ).click(); + await expect( + setupPage.locator( 'p.install-help' ) + ).toBeVisible(); + await expect( + setupPage.locator( 'p.install-help' ) + ).toContainText( + 'If you have a plugin in a .zip format, you may install or update it by uploading it here.' + ); + const [ fileChooser ] = await Promise.all( [ + setupPage.waitForEvent( 'filechooser' ), + setupPage.locator( '#pluginzip' ).click(), + ] ); + await fileChooser.setFiles( woocommerceZipPath ); + console.log( 'Uploading nightly build...' ); + await setupPage + .locator( '#install-plugin-submit' ) + .click( { timeout: 60000 } ); + await setupPage.waitForLoadState( 'networkidle' ); + await expect( + setupPage.getByRole( 'link', { name: 'Activate Plugin' } ) + ).toBeVisible(); + console.log( 'Activating Plugin...' ); + await setupPage + .getByRole( 'link', { name: 'Activate Plugin' } ) + .click( { timeout: 60000 } ); + pluginActive = true; + break; + } catch ( e ) { + console.log( + `Installing and activating plugin failed, Retrying... ${ i }/${ adminRetries }` + ); + console.log( e ); + } + } + if ( ! pluginActive ) { + console.error( + 'Cannot proceed api test, as installing WC failed. Please check if the test site has been setup correctly.' + ); + process.exit( 1 ); + } console.log( 'WooCommerce Re-installed.' ); await expect( @@ -151,6 +178,23 @@ module.exports = async ( config ) => { await deleteZip( woocommerceZipPath ); - // await site.reset( process.env.USER_KEY, process.env.USER_SECRET ); + // Might need to update the database + await setupPage.goto( 'wp-admin/plugins.php' ); + const updateButton = setupPage.locator( + 'text=Update WooCommerce Database' + ); + const updateCompleteMessage = setupPage.locator( + 'text=WooCommerce database update complete.' + ); + await expect( setupPage.locator( 'div.wrap > h1' ) ).toHaveText( + 'Plugins' + ); + if ( await updateButton.isVisible() ) { + console.log( 'Database update button present. Click it.' ); + await updateButton.click( { timeout: 60000 } ); + await expect( updateCompleteMessage ).toBeVisible(); + } else { + console.log( 'No DB update needed' ); + } } }; From 90f03c9711af434e76ea58e87bf76f78c0db2052 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Thu, 7 Sep 2023 13:13:39 -0700 Subject: [PATCH 12/59] Update has propagated, remove conditional --- .../tests/data/data-crud.test.js | 156 ++++++------------ 1 file changed, 50 insertions(+), 106 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js index 02948426ff1..86ebc53540a 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/data/data-crud.test.js @@ -7787,112 +7787,56 @@ test.describe( 'Data API tests', () => { } ), ] ) ); - if ( ! shouldSkip ) { - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - code: 'VEF', - name: 'Venezuelan bolívar (2008–2018)', - symbol: 'Bs F', - _links: { - self: [ - { - href: expect.stringContaining( - 'data/currencies/VEF' - ), - }, - ], - collection: [ - { - href: expect.stringContaining( - 'data/currencies' - ), - }, - ], - }, - } ), - ] ) - ); - } else { - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - code: 'VEF', - name: 'Venezuelan bolívar', - symbol: 'Bs F', - _links: { - self: [ - { - href: expect.stringContaining( - 'data/currencies/VEF' - ), - }, - ], - collection: [ - { - href: expect.stringContaining( - 'data/currencies' - ), - }, - ], - }, - } ), - ] ) - ); - } - if ( ! shouldSkip ) { - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - code: 'VES', - name: 'Venezuelan bolívar', - symbol: 'Bs.', - _links: { - self: [ - { - href: expect.stringContaining( - 'data/currencies/VES' - ), - }, - ], - collection: [ - { - href: expect.stringContaining( - 'data/currencies' - ), - }, - ], - }, - } ), - ] ) - ); - } else { - expect( responseJSON ).toEqual( - expect.arrayContaining( [ - expect.objectContaining( { - code: 'VES', - name: 'Bolívar soberano', - symbol: 'Bs.S', - _links: { - self: [ - { - href: expect.stringContaining( - 'data/currencies/VES' - ), - }, - ], - collection: [ - { - href: expect.stringContaining( - 'data/currencies' - ), - }, - ], - }, - } ), - ] ) - ); - } + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + code: 'VEF', + name: 'Venezuelan bolívar (2008–2018)', + symbol: 'Bs F', + _links: { + self: [ + { + href: expect.stringContaining( + 'data/currencies/VEF' + ), + }, + ], + collection: [ + { + href: expect.stringContaining( + 'data/currencies' + ), + }, + ], + }, + } ), + ] ) + ); + expect( responseJSON ).toEqual( + expect.arrayContaining( [ + expect.objectContaining( { + code: 'VES', + name: 'Venezuelan bolívar', + symbol: 'Bs.', + _links: { + self: [ + { + href: expect.stringContaining( + 'data/currencies/VES' + ), + }, + ], + collection: [ + { + href: expect.stringContaining( + 'data/currencies' + ), + }, + ], + }, + } ), + ] ) + ); expect( responseJSON ).toEqual( expect.arrayContaining( [ expect.objectContaining( { From 24e81b0f20d6aac273687d1c810f098c5132f752 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Thu, 7 Sep 2023 13:27:39 -0700 Subject: [PATCH 13/59] Added some awaits --- .../tests/shipping/shipping-zones.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js index a984099972f..33ce97ed23a 100644 --- a/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js +++ b/plugins/woocommerce/tests/api-core-tests/tests/shipping/shipping-zones.test.js @@ -9,7 +9,7 @@ const shouldSkip = API_BASE_URL != undefined; * @group shipping */ -test.describe( 'Shipping zones API tests', () => { +test.describe.serial( 'Shipping zones API tests', () => { //Shipping zone to be created, retrieved, updated, and deleted by the tests. let shippingZone = getShippingZoneExample(); @@ -169,9 +169,9 @@ test.describe( 'Shipping zones API tests', () => { }); const putResponseStateOnlyJSON = await putResponseStateOnly.json(); - expect( putResponseStateOnly.status() ).toEqual( 200 ); + await expect( putResponseStateOnly.status() ).toEqual( 200 ); - expect( putResponseStateOnlyJSON ).toHaveLength(0); + await expect( putResponseStateOnlyJSON ).toHaveLength(0); } ); @@ -185,15 +185,15 @@ test.describe( 'Shipping zones API tests', () => { const deleteResponseJSON = await deleteResponse.json(); //validate response - expect( deleteResponse.status() ).toEqual( 200 ); - expect( deleteResponseJSON.id ).toEqual( shippingZone.id ); + await expect( deleteResponse.status() ).toEqual( 200 ); + await expect( deleteResponseJSON.id ).toEqual( shippingZone.id ); // only run on wp-env because caching on external hosts makes unreliable if ( ! shouldSkip ) { //call API to attempt to retrieve the deleted shipping zone const response = await request.get( `/wp-json/wc/v3/shipping/zones/${shippingZone.id}`); //validate response - expect( response.status() ).toEqual( 404 ); + await expect( response.status() ).toEqual( 404 ); } } ); From 48016efaa690f739422a8792972a97a05233e293 Mon Sep 17 00:00:00 2001 From: Fernando Marichal Date: Fri, 8 Sep 2023 08:58:34 -0300 Subject: [PATCH 14/59] Fix infinite category loading state (#40073) * Fix loading state * Add changelog * Replace catch with finally --- .../changelog/fix-infinite_categories_loading_state | 4 ++++ .../product-editor/src/blocks/taxonomy/use-taxonomy-search.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/js/product-editor/changelog/fix-infinite_categories_loading_state diff --git a/packages/js/product-editor/changelog/fix-infinite_categories_loading_state b/packages/js/product-editor/changelog/fix-infinite_categories_loading_state new file mode 100644 index 00000000000..51c476201b6 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-infinite_categories_loading_state @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix infinite category loading state diff --git a/packages/js/product-editor/src/blocks/taxonomy/use-taxonomy-search.ts b/packages/js/product-editor/src/blocks/taxonomy/use-taxonomy-search.ts index 4ceefd15112..4ad83a66c51 100644 --- a/packages/js/product-editor/src/blocks/taxonomy/use-taxonomy-search.ts +++ b/packages/js/product-editor/src/blocks/taxonomy/use-taxonomy-search.ts @@ -69,7 +69,7 @@ const useTaxonomySearch = ( taxonomyName ); } - } catch ( e ) { + } finally { setIsSearching( false ); } return taxonomies; From c16bf0f280d1cda7a31a65bc465b0601cabe211f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:06:35 -0700 Subject: [PATCH 15/59] Delete changelog files based on PR 39911 (#40094) Delete changelog files for 39911 Co-authored-by: WooCommerce Bot --- plugins/woocommerce/changelog/fix-cache_data_store | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 plugins/woocommerce/changelog/fix-cache_data_store diff --git a/plugins/woocommerce/changelog/fix-cache_data_store b/plugins/woocommerce/changelog/fix-cache_data_store deleted file mode 100644 index 62ac2f77443..00000000000 --- a/plugins/woocommerce/changelog/fix-cache_data_store +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Update modified date when a metadata is saved for HPOS. From e99a61f9850b3c4ece742bcf85794fc2ff23dbcb Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Fri, 8 Sep 2023 13:56:45 -0700 Subject: [PATCH 16/59] All global setup only runs on daily --- .../tests/api-core-tests/global-setup.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index 85bb1f91fdb..e1d60a063ec 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -1,21 +1,26 @@ -const { chromium, expect } = require( '@playwright/test' ); +if ( + process.env.API_BASE_URL && + ! process.env.API_BASE_URL.includes( 'localhost' ) +) { + const { chromium, expect } = require( '@playwright/test' ); +} const { GITHUB_TOKEN, UPDATE_WC } = process.env; const { downloadZip, deleteZip } = require( './utils/plugin-utils' ); const axios = require( 'axios' ).default; module.exports = async ( config ) => { - const { baseURL, userAgent } = config.projects[ 0 ].use; - const contextOptions = { baseURL, userAgent }; - - const browser = await chromium.launch(); - const setupContext = await browser.newContext( contextOptions ); - const setupPage = await setupContext.newPage(); - // If API_BASE_URL is configured and doesn't include localhost, running on daily host if ( process.env.API_BASE_URL && ! process.env.API_BASE_URL.includes( 'localhost' ) ) { + const { baseURL, userAgent } = config.projects[ 0 ].use; + const contextOptions = { baseURL, userAgent }; + + const browser = await chromium.launch(); + const setupContext = await browser.newContext( contextOptions ); + const setupPage = await setupContext.newPage(); + const getWCDownloadURL = async () => { const requestConfig = { method: 'get', From 1e9a63d15a8f31052f021c38f043f7daa21d50b1 Mon Sep 17 00:00:00 2001 From: Jon Lane Date: Fri, 8 Sep 2023 14:05:03 -0700 Subject: [PATCH 17/59] Reorganize --- plugins/woocommerce/tests/api-core-tests/global-setup.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/plugins/woocommerce/tests/api-core-tests/global-setup.js b/plugins/woocommerce/tests/api-core-tests/global-setup.js index e1d60a063ec..9997f5cedca 100644 --- a/plugins/woocommerce/tests/api-core-tests/global-setup.js +++ b/plugins/woocommerce/tests/api-core-tests/global-setup.js @@ -1,9 +1,3 @@ -if ( - process.env.API_BASE_URL && - ! process.env.API_BASE_URL.includes( 'localhost' ) -) { - const { chromium, expect } = require( '@playwright/test' ); -} const { GITHUB_TOKEN, UPDATE_WC } = process.env; const { downloadZip, deleteZip } = require( './utils/plugin-utils' ); const axios = require( 'axios' ).default; @@ -14,6 +8,8 @@ module.exports = async ( config ) => { process.env.API_BASE_URL && ! process.env.API_BASE_URL.includes( 'localhost' ) ) { + const { chromium, expect } = require( '@playwright/test' ); + const { baseURL, userAgent } = config.projects[ 0 ].use; const contextOptions = { baseURL, userAgent }; From e368dc08264e04f9e7e7e8b1c3a363c3ea6a96eb Mon Sep 17 00:00:00 2001 From: Jeremy Pry Date: Fri, 8 Sep 2023 18:05:38 -0400 Subject: [PATCH 18/59] Update lib/composer.json to PHP 7.4 --- plugins/woocommerce/lib/composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/woocommerce/lib/composer.json b/plugins/woocommerce/lib/composer.json index 0fda68e1664..71400a68e4e 100644 --- a/plugins/woocommerce/lib/composer.json +++ b/plugins/woocommerce/lib/composer.json @@ -4,7 +4,7 @@ "prefer-stable": true, "minimum-stability": "dev", "require": { - "php": ">=7.2" + "php": ">=7.4" }, "require-dev": { "league/container": "3.3.5", @@ -12,7 +12,7 @@ }, "config": { "platform": { - "php": "7.2" + "php": "7.4" } }, "scripts": { From da2d9755e90052a53f802442a936f5d1de9e6b27 Mon Sep 17 00:00:00 2001 From: Jeremy Pry Date: Fri, 8 Sep 2023 18:05:47 -0400 Subject: [PATCH 19/59] Run composer update --- plugins/woocommerce/lib/composer.lock | 22 +++++++++---------- .../Container/ContainerExceptionInterface.php | 4 +++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/woocommerce/lib/composer.lock b/plugins/woocommerce/lib/composer.lock index 348867f645b..5322b618a79 100644 --- a/plugins/woocommerce/lib/composer.lock +++ b/plugins/woocommerce/lib/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee5352d3dc7eff84b896ab4900209e53", + "content-hash": "08afc0d13426146cf441a311818703f5", "packages": [], "packages-dev": [ { @@ -88,20 +88,20 @@ }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -130,9 +130,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2021-11-05T16:50:12+00:00" } ], "aliases": [], @@ -141,11 +141,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.2" + "php": ">=7.4" }, "platform-dev": [], "platform-overrides": { - "php": "7.2" + "php": "7.4" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.6.0" } diff --git a/plugins/woocommerce/lib/packages/Psr/Container/ContainerExceptionInterface.php b/plugins/woocommerce/lib/packages/Psr/Container/ContainerExceptionInterface.php index dfadc2354db..b7ee1f49a69 100644 --- a/plugins/woocommerce/lib/packages/Psr/Container/ContainerExceptionInterface.php +++ b/plugins/woocommerce/lib/packages/Psr/Container/ContainerExceptionInterface.php @@ -2,9 +2,11 @@ namespace Automattic\WooCommerce\Vendor\Psr\Container; +use Throwable; + /** * Base interface representing a generic exception in a container. */ -interface ContainerExceptionInterface +interface ContainerExceptionInterface extends Throwable { } From 84c9e7551c9ed0b67e19412890c00e9b6e8c3809 Mon Sep 17 00:00:00 2001 From: Jeremy Pry Date: Fri, 8 Sep 2023 18:48:53 -0400 Subject: [PATCH 20/59] Add changelog file --- plugins/woocommerce/changelog/bug-php-7.4-lib | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 plugins/woocommerce/changelog/bug-php-7.4-lib diff --git a/plugins/woocommerce/changelog/bug-php-7.4-lib b/plugins/woocommerce/changelog/bug-php-7.4-lib new file mode 100644 index 00000000000..5fc4f8fdfc0 --- /dev/null +++ b/plugins/woocommerce/changelog/bug-php-7.4-lib @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: Bump PHP version where it was missed in #39820. + + From 914df4b921acc91c6ad8612449216e18d49ac0a5 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Sat, 9 Sep 2023 11:05:31 +0800 Subject: [PATCH 21/59] Fix customize store whitescreen in WP 6.3 (#40031) * Fix wp-router dependencies issue * Add changefile(s) from automation for the following project(s): woocommerce * Fix customize your store task header button (#40033) * Fix customize your store task header button * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --------- Co-authored-by: github-actions --- .../components/task-headers/customize-store.tsx | 9 +++++++-- plugins/woocommerce-admin/webpack.config.js | 6 ------ plugins/woocommerce/changelog/fix-cys-task-header-button | 4 ++++ plugins/woocommerce/changelog/fix-cys-wp-6-3-router-bug | 4 ++++ plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php | 7 +++++++ 5 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-cys-task-header-button create mode 100644 plugins/woocommerce/changelog/fix-cys-wp-6-3-router-bug diff --git a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/customize-store.tsx b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/customize-store.tsx index d76847d6894..bb2e4233c2f 100644 --- a/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/customize-store.tsx +++ b/plugins/woocommerce-admin/client/task-lists/setup-task-list/components/task-headers/customize-store.tsx @@ -4,6 +4,7 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { TaskType } from '@woocommerce/data'; +import { getAdminLink } from '@woocommerce/settings'; /** * Internal dependencies @@ -12,7 +13,6 @@ import { WC_ASSET_URL } from '../../../../utils/admin-settings'; const CustomizeStoreHeader = ( { task, - goToTask, }: { task: TaskType; goToTask: React.MouseEventHandler; @@ -40,7 +40,12 @@ const CustomizeStoreHeader = ( { diff --git a/plugins/woocommerce-admin/webpack.config.js b/plugins/woocommerce-admin/webpack.config.js index 25512a352ab..6a97080e6b7 100644 --- a/plugins/woocommerce-admin/webpack.config.js +++ b/plugins/woocommerce-admin/webpack.config.js @@ -217,12 +217,6 @@ const webpackConfig = { return null; } - if ( request === '@wordpress/router' ) { - // The external wp.router does not exist in WP 6.2 and below, so we need to skip requesting to external here. - // We use the router in the customize store. We can remove this once our minimum support is WP 6.3. - return null; - } - if ( request.startsWith( '@wordpress/edit-site' ) ) { // The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite. // We use the edit-site components in the customize store. diff --git a/plugins/woocommerce/changelog/fix-cys-task-header-button b/plugins/woocommerce/changelog/fix-cys-task-header-button new file mode 100644 index 00000000000..94089c09e9b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cys-task-header-button @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix customize your store task header button diff --git a/plugins/woocommerce/changelog/fix-cys-wp-6-3-router-bug b/plugins/woocommerce/changelog/fix-cys-wp-6-3-router-bug new file mode 100644 index 00000000000..7aa8333f3bc --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cys-wp-6-3-router-bug @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix customize store white screen bug in WP 6.3 diff --git a/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php b/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php index 31377ebfe5b..008052d3ad4 100644 --- a/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php +++ b/plugins/woocommerce/src/Internal/Admin/WCAdminAssets.php @@ -297,6 +297,13 @@ class WCAdminAssets { $script_assets_filename = self::get_script_asset_filename( $script_path_name, 'index' ); $script_assets = require WC_ADMIN_ABSPATH . WC_ADMIN_DIST_JS_FOLDER . $script_path_name . '/' . $script_assets_filename; + global $wp_version; + if ( 'app' === $script_path_name && version_compare( $wp_version, '6.3', '<' ) ) { + // Remove wp-router dependency for WordPress versions < 6.3 because wp-router is not included in those versions. We only use wp-router in customize store pages and the feature is only available in WordPress 6.3+. + // We can remove this once our minimum support is WP 6.3. + $script_assets['dependencies'] = array_diff( $script_assets['dependencies'], array( 'wp-router' ) ); + } + wp_register_script( $script, self::get_url( $script_path_name . '/index', 'js' ), From 2fc02016327d7895af46601221056da3634c2bc5 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Mon, 11 Sep 2023 15:13:40 +0800 Subject: [PATCH 22/59] Fix CYS `__experimentalReapplyBlockTypeFilters` is not a function (#40104) * Fix __experimentalReapplyBlockTypeFilters is not a function * Add changefile(s) from automation for the following project(s): woocommerce Update changelog --------- Co-authored-by: github-actions --- .../client/customize-store/assembler-hub/index.tsx | 9 +++++++-- .../changelog/fix-cys-reapply-block-type-filters-error | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-cys-reapply-block-type-filters-error diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx index 3d2b399b662..ea8571aa2b2 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/index.tsx @@ -92,8 +92,13 @@ export const AssemblerHub: CustomizeStoreComponent = ( props ) => { ) => fetchLinkSuggestions( search, searchOptions, settings ); settings.__experimentalFetchRichUrlData = fetchUrlData; - // @ts-ignore No types for this exist yet. - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + const reapplyBlockTypeFilters = + // @ts-ignore No types for this exist yet. + dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters || // GB < 16.6 + // @ts-ignore No types for this exist yet. + dispatch( blocksStore ).reapplyBlockTypeFilters; // GB >= 16.6 + reapplyBlockTypeFilters(); + const coreBlocks = __experimentalGetCoreBlocks().filter( ( { name }: { name: string } ) => name !== 'core/freeform' && ! getBlockType( name ) diff --git a/plugins/woocommerce/changelog/fix-cys-reapply-block-type-filters-error b/plugins/woocommerce/changelog/fix-cys-reapply-block-type-filters-error new file mode 100644 index 00000000000..d6126b8ec9b --- /dev/null +++ b/plugins/woocommerce/changelog/fix-cys-reapply-block-type-filters-error @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix CYS `__experimentalReapplyBlockTypeFilters` is not a function From 940b24cce2e9518603f736d844ed6b7d8ba22b60 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Mon, 11 Sep 2023 17:48:23 +0800 Subject: [PATCH 23/59] Add customize store colors (#40051) * Add cys color palettes * Add reference links * Add changefile(s) from automation for the following project(s): woocommerce * Rename color-palette * Add changefile(s) from automation for the following project(s): woocommerce * Update folder structure * Reset color changes when navigating back * Rename files * Update COLOR_PALETTES options * Add color-palette-variations/preview.tsx * Fix double slash links * Fix color variations item style --------- Co-authored-by: github-actions --- .../color-palette-variations/constants.ts | 561 ++++++++++++++++++ .../color-palette-variations/index.tsx | 31 + .../color-palette-variations/preview.tsx | 144 +++++ .../sidebar/global-styles/color-panel.jsx | 33 ++ .../global-styles-variation-iframe/index.tsx | 98 +++ .../global-styles-variation-iframe/style.scss | 82 +++ .../sidebar/global-styles/index.ts | 2 + .../global-styles/variation-container.jsx | 83 +++ .../assembler-hub/sidebar/save-hub.tsx | 14 + ...idebar-navigation-screen-color-palette.tsx | 57 +- .../sidebar-navigation-screen-footer.tsx | 2 +- .../sidebar-navigation-screen-header.tsx | 2 +- .../sidebar-navigation-screen-homepage.tsx | 2 +- .../sidebar-navigation-screen-main.tsx | 2 +- .../sidebar-navigation-screen-pages.tsx | 4 +- .../sidebar-navigation-screen-typography.tsx | 4 +- .../customize-store/assembler-hub/style.scss | 69 ++- .../changelog/add-cys-color-palette | 4 + 18 files changed, 1175 insertions(+), 19 deletions(-) create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/constants.ts create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/index.tsx create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/preview.tsx create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/index.tsx create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/style.scss create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx create mode 100644 plugins/woocommerce/changelog/add-cys-color-palette diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/constants.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/constants.ts new file mode 100644 index 00000000000..2ab392fb6ab --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/constants.ts @@ -0,0 +1,561 @@ +// TODO: Fetch AI-picked color palettes from the backend API +export const COLOR_PALETTES = [ + { + title: 'Ancient Bronze', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#11163d', + name: 'Primary', + slug: 'primary', + }, + { + color: '#8C8369', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#11163d', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#ffffff', + name: 'Background', + slug: 'background', + }, + { + color: '#F7F2EE', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--secondary)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + }, + }, + }, + }, + wpcom_category: 'Neutral', + }, + { + title: 'Crimson Tide', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#A02040', + name: 'Primary', + slug: 'primary', + }, + { + color: '#234B57', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#871C37', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#ffffff', + name: 'Background', + slug: 'background', + }, + { + color: '#FCE5DF', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--secondary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--secondary)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + }, + }, + }, + }, + wpcom_category: 'Neutral', + }, + { + title: 'Midnight Citrus', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#1B1736', + name: 'Primary', + slug: 'primary', + }, + { + color: '#7E76A3', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#1B1736', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#ffffff', + name: 'Background', + slug: 'background', + }, + { + color: '#E9FC5F', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Neutral', + }, + { + title: 'Fuchsia', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#b7127f', + name: 'Primary', + slug: 'primary', + }, + { + color: '#18020C', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#b7127f', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#f7edf6', + name: 'Background', + slug: 'background', + }, + { + color: '#ffffff', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--foreground)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Bright', + }, + { + title: 'Raspberry Chocolate', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#42332e', + name: 'Primary', + slug: 'primary', + }, + { + color: '#d64d68', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#241d1a', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#eeeae6', + name: 'Background', + slug: 'background', + }, + { + color: '#D6CCC2', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--secondary)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Bright', + }, + { + title: 'Gumtree Sunset', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#476C77', + name: 'Primary', + slug: 'primary', + }, + { + color: '#EFB071', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#476C77', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#edf4f4', + name: 'Background', + slug: 'background', + }, + { + color: '#ffffff', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--tertiary)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Bright', + }, + { + title: 'Ice', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + slug: 'primary', + color: '#12123F', + name: 'Primary', + }, + { + slug: 'secondary', + color: '#3473FE', + name: 'Secondary', + }, + { + slug: 'foreground', + color: '#12123F', + name: 'Foreground', + }, + { + slug: 'background', + color: '#F1F4FA', + name: 'Background', + }, + { + slug: 'tertiary', + color: '#DBE6EE', + name: 'Tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Bright', + }, + { + title: 'Sandalwood Oasis', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#F0EBE3', + name: 'Primary', + slug: 'primary', + }, + { + color: '#DF9785', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#ffffff', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#2a2a16', + name: 'Background', + slug: 'background', + }, + { + color: '#434323', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--secondary)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Dark', + }, + { + title: 'Lilac Nightshade', + version: 2, + settings: { + color: { + palette: { + theme: [ + { + color: '#f5d6ff', + name: 'Primary', + slug: 'primary', + }, + { + color: '#C48DDA', + name: 'Secondary', + slug: 'secondary', + }, + { + color: '#ffffff', + name: 'Foreground', + slug: 'foreground', + }, + { + color: '#000000', + name: 'Background', + slug: 'background', + }, + { + color: '#462749', + name: 'Tertiary', + slug: 'tertiary', + }, + ], + }, + }, + }, + styles: { + color: { + background: 'var(--wp--preset--color--background)', + text: 'var(--wp--preset--color--foreground)', + }, + elements: { + button: { + color: { + background: 'var(--wp--preset--color--primary)', + text: 'var(--wp--preset--color--background)', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--foreground)', + }, + ':hover': { + color: { + text: 'var(--wp--preset--color--primary)', + }, + }, + }, + }, + }, + wpcom_category: 'Dark', + }, +]; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/index.tsx new file mode 100644 index 00000000000..5cb2ea582d6 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/index.tsx @@ -0,0 +1,31 @@ +// Reference: https://github.com/WordPress/gutenberg/blob/d5ab7238e53d0947d4bb0853464b1c58325b6130/packages/edit-site/src/components/global-styles/style-variations-container.js +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +// @ts-ignore No types for this exist yet. +import { __experimentalGrid as Grid } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { COLOR_PALETTES } from './constants'; +import { VariationContainer } from '../variation-container'; +import { ColorPaletteVariationPreview } from './preview'; + +export const ColorPalette = () => { + return ( + + { COLOR_PALETTES.map( ( variation, index ) => ( + + + + ) ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/preview.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/preview.tsx new file mode 100644 index 00000000000..c86e6231ddf --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-palette-variations/preview.tsx @@ -0,0 +1,144 @@ +// Reference: https://github.com/Automattic/wp-calypso/blob/d3c9b16fb99ce242f61baa21119b7c20f8823be6/packages/global-styles/src/components/color-palette-variations/preview.tsx#L20 +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +import { + privateApis as blockEditorPrivateApis, + // @ts-ignore no types exist yet. +} from '@wordpress/block-editor'; +import { + // @ts-ignore No types for this exist yet. + __experimentalHStack as HStack, + // @ts-ignore No types for this exist yet. + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { useResizeObserver } from '@wordpress/compose'; +// @ts-ignore No types for this exist yet. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { GlobalStylesVariationIframe } from '../global-styles-variation-iframe'; + +export interface Color { + color: string; + name: string; + slug: string; +} + +const STYLE_PREVIEW_HEIGHT = 44; +const STYLE_PREVIEW_COLOR_SWATCH_SIZE = 16; + +const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorPrivateApis ); + +interface Props { + title?: string; +} + +export const ColorPaletteVariationPreview = ( { title }: Props ) => { + const [ fontWeight ] = useGlobalStyle( 'typography.fontWeight' ); + const [ fontFamily = 'serif' ] = useGlobalStyle( 'typography.fontFamily' ); + const [ headingFontFamily = fontFamily ] = useGlobalStyle( + 'elements.h1.typography.fontFamily' + ); + const [ headingFontWeight = fontWeight ] = useGlobalStyle( + 'elements.h1.typography.fontWeight' + ); + const [ textColor = 'black' ] = useGlobalStyle( 'color.text' ); + const [ headingColor = textColor ] = useGlobalStyle( + 'elements.h1.color.text' + ); + const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); + const [ gradientValue ] = useGlobalStyle( 'color.gradient' ); + const [ themeColors ] = useGlobalSetting( 'color.palette.theme' ); + const [ containerResizeListener, { width } ] = useResizeObserver(); + const normalizedHeight = STYLE_PREVIEW_HEIGHT; + const normalizedSwatchSize = STYLE_PREVIEW_COLOR_SWATCH_SIZE; + const uniqueColors = [ + ...new Set< string >( + themeColors.map( ( { color }: Color ) => color ) + ), + ]; + const highlightedColors = uniqueColors + .filter( + // we exclude background color because it is already visible in the preview. + ( color ) => color !== backgroundColor + ) + .slice( 0, 2 ); + + return ( + +
+
+ { title ? ( + + { highlightedColors.map( ( color, index ) => ( +
+ ) ) } + + ) : ( + +
+ { __( 'Default', 'woocommerce' ) } +
+
+ ) } +
+
+ + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx new file mode 100644 index 00000000000..c6107b9d25c --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/color-panel.jsx @@ -0,0 +1,33 @@ +// Reference: https://github.com/WordPress/gutenberg/blob/f9e405e0e53d61cd36af4f3b34f2de75874de1e1/packages/edit-site/src/components/global-styles/screen-colors.js#L23 +/** + * External dependencies + */ +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; + +const { + useGlobalStyle, + useGlobalSetting, + useSettingsForBlockElement, + ColorPanel: StylesColorPanel, +} = unlock( blockEditorPrivateApis ); + +export const ColorPanel = () => { + const [ style ] = useGlobalStyle( '', undefined, 'user', { + shouldDecodeEncode: false, + } ); + const [ inheritedStyle, setStyle ] = useGlobalStyle( '', undefined, 'all', { + shouldDecodeEncode: false, + } ); + const [ rawSettings ] = useGlobalSetting( '' ); + const settings = useSettingsForBlockElement( rawSettings ); + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/index.tsx new file mode 100644 index 00000000000..eb3240ed47a --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/index.tsx @@ -0,0 +1,98 @@ +// Reference: https://github.com/Automattic/wp-calypso/blob/d3c9b16fb99ce242f61baa21119b7c20f8823be6/packages/global-styles/src/components/global-styles-variation-container/index.tsx#L19 +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +import { + __unstableIframe as Iframe, + __unstableEditorStyles as EditorStyles, + privateApis as blockEditorPrivateApis, + // @ts-ignore no types exist yet. +} from '@wordpress/block-editor'; +import { useRefEffect } from '@wordpress/compose'; +import { useMemo } from 'react'; +// @ts-ignore No types for this exist yet. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; + +/** + * Internal dependencies + */ +import './style.scss'; + +const { useGlobalStylesOutput } = unlock( blockEditorPrivateApis ); + +interface Props { + width: number | null; + height: number; + inlineCss?: string; + containerResizeListener: JSX.Element; + children: JSX.Element; + onFocusOut?: () => void; +} + +export const GlobalStylesVariationIframe = ( { + width, + height, + inlineCss, + containerResizeListener, + children, + onFocusOut, + ...props +}: Props ) => { + const [ styles ] = useGlobalStylesOutput(); + // Reset leaked styles from WP common.css and remove main content layout padding and border. + const editorStyles = useMemo( () => { + if ( styles ) { + return [ + ...styles, + ...( inlineCss + ? [ + { + css: inlineCss, + isGlobalStyles: true, + }, + ] + : [] ), + { + css: 'html{overflow:hidden}body{min-width: 0;padding: 0;border: none;transform:scale(1);}', + isGlobalStyles: true, + }, + ]; + } + return styles; + }, [ inlineCss, styles ] ); + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/style.scss new file mode 100644 index 00000000000..3e356e2b532 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/global-styles-variation-iframe/style.scss @@ -0,0 +1,82 @@ +.global-styles-variation-container__iframe { + border-radius: 3px; /* stylelint-disable-line scales/radii */ + box-shadow: 0 0 0 1px rgb(0 0 0 / 10%); + border: 0; + display: block; + max-width: 100%; +} + +.color-block-support-panel { + padding: 0; + border-top: 0; + + .components-tools-panel-header { + display: none; + } + + .block-editor-tools-panel-color-gradient-settings__dropdown { + .components-button { + color: $gray-900; + font-size: 0.8125rem; + font-style: normal; + font-weight: 400; + line-height: 16px; /* 123.077% */ + + &.is-open { + background: initial; + color: $gray-900; + } + + &:focus { + box-shadow: none; + } + } + } + + .block-editor-panel-color-gradient-settings__dropdown { + padding: 12px 0; + + .components-flex { + flex-direction: row-reverse; + justify-content: space-between; + } + } + .color-block-support-panel__inner-wrapper { + .block-editor-tools-panel-color-gradient-settings__item { + border: 0; + order: 10; // default + + // Background + &:nth-child(2) { + order: 1; + } + + // Text + &.first { + border-top: 0; + order: 2; + } + + // Heading + &:nth-child(6) { + order: 3; + } + + // Button + &:nth-child(5) { + order: 4; + } + + // Link + &:nth-child(3) { + order: 5; + } + } + } + + .block-editor-color-gradient-control__panel { + > .components-flex > .components-h-stack.components-v-stack { + display: none; + } + } +} diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts new file mode 100644 index 00000000000..5aee47c9b64 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts @@ -0,0 +1,2 @@ +export { ColorPalette } from './color-palette-variations'; +export { ColorPanel } from './color-panel'; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx new file mode 100644 index 00000000000..088bb2740e0 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx @@ -0,0 +1,83 @@ +// Reference: https://github.com/WordPress/gutenberg/blob/d5ab7238e53d0947d4bb0853464b1c58325b6130/packages/edit-site/src/components/global-styles/style-variations-container.js +/** + * External dependencies + */ +import classnames from 'classnames'; +import { useMemo, useContext } from '@wordpress/element'; +import { ENTER } from '@wordpress/keycodes'; +import { __, sprintf } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider'; +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; + +const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( + blockEditorPrivateApis +); + +export const VariationContainer = ( { variation, children } ) => { + const { base, user, setUserConfig } = useContext( GlobalStylesContext ); + const context = useMemo( () => { + return { + user: { + settings: variation.settings ?? {}, + styles: variation.styles ?? {}, + }, + base, + merged: mergeBaseAndUserConfigs( base, variation ), + setUserConfig: () => {}, + }; + }, [ variation, base ] ); + + const selectVariation = () => { + setUserConfig( () => { + return { + settings: variation.settings, + styles: variation.styles, + }; + } ); + }; + + const selectOnEnter = ( event ) => { + if ( event.keyCode === ENTER ) { + event.preventDefault(); + selectVariation(); + } + }; + + const isActive = useMemo( () => { + return areGlobalStyleConfigsEqual( user, variation ); + }, [ user, variation ] ); + + let label = variation?.title; + if ( variation?.description ) { + label = sprintf( + /* translators: %1$s: variation title. %2$s variation description. */ + __( '%1$s (%2$s)', 'woocommerce' ), + variation?.title, + variation?.description + ); + } + + return ( + +
+
+ { children } +
+
+
+ ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx index 3611ad0f679..4fe0c1f7f80 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/save-hub.tsx @@ -111,6 +111,20 @@ export const SaveHub = () => { }, { undoIgnore: true } ); + } else if ( + entity.kind === 'root' && + entity.name === 'globalStyles' + ) { + editEntityRecord( + entity.kind, + entity.name, + entity.key, + { + styles: undefined, + settings: undefined, + }, + { undoIgnore: true } + ); } else { editEntityRecord( entity.kind, diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx index dbce08bc09f..d795f27b2a2 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-color-palette.tsx @@ -1,15 +1,62 @@ +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /** * External dependencies */ import { __ } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; import { Link } from '@woocommerce/components'; +import { useSelect } from '@wordpress/data'; +// @ts-ignore no types exist yet. +import { BlockEditorProvider } from '@wordpress/block-editor'; +import { noop } from 'lodash'; +// @ts-ignore No types for this exist yet. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; +// @ts-ignore No types for this exist yet. +import { store as editSiteStore } from '@wordpress/edit-site/build-module/store'; +import { PanelBody } from '@wordpress/components'; /** * Internal dependencies */ import { SidebarNavigationScreen } from './sidebar-navigation-screen'; import { ADMIN_URL } from '~/utils/admin-settings'; +import { ColorPalette, ColorPanel } from './global-styles'; + +const SidebarNavigationScreenColorPaletteContent = () => { + const { storedSettings } = useSelect( ( select ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + + return { + storedSettings: getSettings( false ), + }; + }, [] ); + + // Wrap in a BlockEditorProvider to ensure that the Iframe's dependencies are + // loaded. This is necessary because the Iframe component waits until + // the block editor store's `__internalIsInitialized` is true before + // rendering the iframe. Without this, the iframe previews will not render + // in mobile viewport sizes, where the editor canvas is hidden. + return ( +
+ + + + + + + +
+ ); +}; export const SidebarNavigationScreenColorPalette = () => { return ( @@ -23,23 +70,19 @@ export const SidebarNavigationScreenColorPalette = () => { { EditorLink: ( ), StyleLink: ( ), } ) } - content={ - <> -
- - } + content={ } /> ); }; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx index 4547d77ab26..36335e0d6a2 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-footer.tsx @@ -22,7 +22,7 @@ export const SidebarNavigationScreenFooter = () => { { EditorLink: ( ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx index cb74d1fc43d..76f94a12977 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-header.tsx @@ -22,7 +22,7 @@ export const SidebarNavigationScreenHeader = () => { { EditorLink: ( ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx index 095cf8f5d8b..7a6bf1acd98 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-homepage.tsx @@ -22,7 +22,7 @@ export const SidebarNavigationScreenHomepage = () => { { EditorLink: ( ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-main.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-main.tsx index cc0036fb23a..833f63853c6 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-main.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-main.tsx @@ -45,7 +45,7 @@ export const SidebarNavigationScreenMain = () => { { EditorLink: ( ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-pages.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-pages.tsx index 34b1cbb0565..62e275ba3ff 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-pages.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-pages.tsx @@ -23,13 +23,13 @@ export const SidebarNavigationScreenPages = () => { { EditorLink: ( ), PageLink: ( ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx index b1f66f8d2a8..8353edcc424 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx @@ -23,13 +23,13 @@ export const SidebarNavigationScreenTypography = () => { { EditorLink: ( ), StyleLink: ( ), diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss index 8fc521b1598..05e760eb9f3 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss @@ -113,12 +113,16 @@ /* Sidebar */ .edit-site-layout__sidebar-region { width: 380px; + z-index: 3; } .edit-site-layout__sidebar { - .edit-site-sidebar__content > div { - padding: 0 16px; - overflow-x: hidden; + .edit-site-sidebar__content { + .components-navigator-screen { + will-change: auto; + padding: 0 16px; + overflow-x: hidden; + } } .edit-site-sidebar-button { @@ -194,7 +198,11 @@ .edit-site-save-hub { border-top: 0; - padding: 32px 29px 32px 35px; + padding: 32px 29px 32px 32px; + + button.is-primary:disabled { + opacity: 0.5; + } } } @@ -313,6 +321,44 @@ } } + /* Color sidebar */ + + .woocommerce-customize-store__color-panel-container { + border: 0; + margin-top: 24px; + padding: 0; + + .components-panel__body-title { + margin: 0 !important; + + &:hover { + background: initial; + } + } + + .components-panel__body-toggle.components-button { + padding: 16px 0; + text-transform: uppercase; + color: $gray-900; + font-size: 0.6875rem; + font-weight: 600; + line-height: 16px; /* 145.455% */ + + &:focus { + box-shadow: none; + } + + .components-panel__arrow { + right: 0; + color: $gray-900; + } + } + } + + .woocommerce-customize-store_sidebar-color-content { + width: 324px; + } + /* Preview Canvas */ .edit-site-layout__canvas { bottom: 16px; @@ -427,3 +473,18 @@ margin-left: 12px; } } + +.woocommerce-customize-store_global-styles-variations_item { + border-radius: 2px; + padding: 2.5px; + + .woocommerce-customize-store_global-styles-variations_item-preview { + border: 1px solid #dcdcde; + background: #fff; + } + + &:hover, + &.is-active { + box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color), 0 0 0 2.5px #fff; + } +} diff --git a/plugins/woocommerce/changelog/add-cys-color-palette b/plugins/woocommerce/changelog/add-cys-color-palette new file mode 100644 index 00000000000..96ca0128d0a --- /dev/null +++ b/plugins/woocommerce/changelog/add-cys-color-palette @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add customize store color palettes From 2273e297f16067b5cea32be9c30cce76a7e66a1a Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Mon, 11 Sep 2023 15:54:59 +0530 Subject: [PATCH 24/59] [HPOS] Modify query to have less characters before the `FROM` keyword. --- plugins/woocommerce/changelog/fix-37362 | 4 + .../Orders/OrdersTableDataStore.php | 89 +++++++++++++---- .../Orders/OrdersTableDataStoreTests.php | 99 ++++++++++++++++++- 3 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-37362 diff --git a/plugins/woocommerce/changelog/fix-37362 b/plugins/woocommerce/changelog/fix-37362 new file mode 100644 index 00000000000..9a87d494dd7 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-37362 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +[HPOS] Modify query to have less characters before the `FROM` keyword. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index bbe90ccce78..05069f53cf4 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -516,13 +516,22 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements return $this->all_order_column_mapping; } + /** + * Helper function to get alias for order table, this is used in select query. + * + * @return string Alias. + */ + private function get_order_table_alias() : string { + return 'o'; + } + /** * Helper function to get alias for op table, this is used in select query. * * @return string Alias. */ private function get_op_table_alias() : string { - return 'order_operational_data'; + return 'p'; } /** @@ -533,7 +542,7 @@ class OrdersTableDataStore extends \Abstract_WC_Order_Data_Store_CPT implements * @return string Alias. */ private function get_address_table_alias( string $type ) : string { - return "address_$type"; + return 'billing' === $type ? 'b' : 's'; } /** @@ -1123,8 +1132,8 @@ WHERE $data_sync_enabled = apply_filters( 'woocommerce_hpos_enable_sync_on_read', $data_sync_enabled ); } - $load_posts_for = array_diff( $order_ids, array_merge( self::$reading_order_ids, self::$backfilling_order_ids ) ); - $post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array(); + $load_posts_for = array_diff( $order_ids, array_merge( self::$reading_order_ids, self::$backfilling_order_ids ) ); + $post_orders = $data_sync_enabled ? $this->get_post_orders_for_ids( array_intersect_key( $orders, array_flip( $load_posts_for ) ) ) : array(); foreach ( $data as $order_data ) { $order_id = absint( $order_data->id ); @@ -1518,14 +1527,19 @@ WHERE * @return \stdClass[]|object|null DB Order objects or error. */ protected function get_order_data_for_ids( $ids ) { - if ( ! $ids ) { + global $wpdb; + + if ( ! $ids || empty( $ids ) ) { return array(); } - global $wpdb; - if ( empty( $ids ) ) { - return array(); - } + $table_aliases = array( + 'orders' => $this->get_order_table_alias(), + 'billing_address' => $this->get_address_table_alias( 'billing' ), + 'shipping_address' => $this->get_address_table_alias( 'shipping' ), + 'operational_data' => $this->get_op_table_alias(), + ); + $order_table_alias = $table_aliases['orders']; $order_table_query = $this->get_order_table_select_statement(); $id_placeholder = implode( ', ', array_fill( 0, count( $ids ), '%d' ) ); $order_meta_table = self::get_meta_table_name(); @@ -1533,7 +1547,7 @@ WHERE // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $order_table_query is autogenerated and should already be prepared. $table_data = $wpdb->get_results( $wpdb->prepare( - "$order_table_query WHERE wc_order.id in ( $id_placeholder )", + "$order_table_query WHERE $order_table_alias.id in ( $id_placeholder )", $ids ) ); @@ -1548,9 +1562,27 @@ WHERE $ids ) ); + foreach ( $table_data as $table_datum ) { - $order_data[ $table_datum->id ] = $table_datum; - $order_data[ $table_datum->id ]->meta_data = array(); + $id = $table_datum->{"{$order_table_alias}_id"}; + $order_data[ $id ] = new \stdClass(); + foreach ( $this->get_all_order_column_mappings() as $table_name => $column_mappings ) { + $table_alias = $table_aliases[ $table_name ]; + // This remapping is required to keep the query length small enough to be supported by implementations such as HyperDB (i.e. fetching some tables in join via alias.*, while others via full name). We can revert this commit if HyperDB starts supporting SRTM for query length more than 3076 characters. + foreach ( $column_mappings as $field => $map ) { + $field_name = $map['name'] ?? "{$table_name}_$field"; + if ( property_exists( $table_datum, $field_name ) ) { + $field_value = $table_datum->{ $field_name }; // Unique column, field name is different prop name. + } elseif ( property_exists( $table_datum, "{$table_alias}_$field" ) ) { + $field_value = $table_datum->{"{$table_alias}_$field"}; // Non-unique column (billing, shipping etc). + } else { + $field_value = $table_datum->{ $field }; // Unique column, field name is same as prop name. + } + $order_data[ $id ]->{$field_name} = $field_value; + } + } + $order_data[ $id ]->id = $id; + $order_data[ $id ]->meta_data = array(); } foreach ( $meta_data as $meta_datum ) { @@ -1572,8 +1604,7 @@ WHERE */ private function get_order_table_select_statement() { $order_table = $this::get_orders_table_name(); - $order_table_alias = 'wc_order'; - $select_clause = $this->generate_select_clause_for_props( $order_table_alias, $this->order_column_mapping ); + $order_table_alias = $this->get_order_table_alias(); $billing_address_table_alias = $this->get_address_table_alias( 'billing' ); $shipping_address_table_alias = $this->get_address_table_alias( 'shipping' ); $op_data_table_alias = $this->get_op_table_alias(); @@ -1581,8 +1612,12 @@ WHERE $shipping_address_clauses = $this->join_shipping_address_table_to_order_query( $order_table_alias, $shipping_address_table_alias ); $operational_data_clauses = $this->join_operational_data_table_to_order_query( $order_table_alias, $op_data_table_alias ); + /** + * We fully spell out address table columns because they have duplicate columns for billing and shipping and would be overwritten if we don't spell them out. There is not such duplication in the operational data table and orders table, so select with `alias`.* is fine. + * We do spell ID columns manually, as they are duplicate. + */ return " -SELECT $select_clause, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, {$operational_data_clauses['select']} +SELECT $order_table_alias.id as o_id, $op_data_table_alias.id as p_id, $order_table_alias.*, {$billing_address_clauses['select']}, {$shipping_address_clauses['select']}, $op_data_table_alias.* FROM $order_table $order_table_alias LEFT JOIN {$billing_address_clauses['join']} LEFT JOIN {$shipping_address_clauses['join']} @@ -1870,8 +1905,20 @@ FROM $order_meta_table if ( $row ) { $result[] = array( 'table' => self::get_orders_table_name(), - 'data' => array_merge( $row['data'], array( 'id' => $order->get_id(), 'type' => $order->get_type() ) ), - 'format' => array_merge( $row['format'], array( 'id' => '%d', 'type' => '%s' ) ), + 'data' => array_merge( + $row['data'], + array( + 'id' => $order->get_id(), + 'type' => $order->get_type(), + ) + ), + 'format' => array_merge( + $row['format'], + array( + 'id' => '%d', + 'type' => '%s', + ) + ), ); } @@ -2328,7 +2375,7 @@ FROM $order_meta_table $this->persist_save( $order ); // Do not fire 'woocommerce_new_order' for draft statuses for backwards compatibility. - if ( 'auto-draft' === $order->get_status( 'edit') ) { + if ( 'auto-draft' === $order->get_status( 'edit' ) ) { return; } @@ -2797,7 +2844,7 @@ CREATE TABLE $meta_table ( /** * Deletes meta based on meta ID. * - * @param WC_Data $object WC_Data object. + * @param WC_Data $object WC_Data object. * @param \stdClass $meta (containing at least ->id). * * @return bool @@ -2827,7 +2874,7 @@ CREATE TABLE $meta_table ( /** * Add new piece of meta. * - * @param WC_Data $object WC_Data object. + * @param WC_Data $object WC_Data object. * @param \stdClass $meta (containing ->key and ->value). * * @return int|bool meta ID or false on failure @@ -2851,7 +2898,7 @@ CREATE TABLE $meta_table ( * @param WC_Data $object WC_Data object. * @param \stdClass $meta (containing ->id, ->key and ->value). * - * @return + * @return bool The number of rows updated, or false on error. */ public function update_meta( &$object, $meta ) { $update_meta = $this->data_store_meta->update_meta( $object, $meta ); diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index f7dd3721bf4..c647b84169b 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -2677,7 +2677,8 @@ class OrdersTableDataStoreTests extends HposTestCase { $this->toggle_cot_authoritative( $cot_is_authoritative ); $this->disable_cot_sync(); - $new_count = $update_count = 0; + $new_count = 0; + $update_count = 0; $callback = function( $order_id ) use ( &$new_count, &$update_count ) { $new_count += 'woocommerce_new_order' === current_action() ? 1 : 0; @@ -2745,7 +2746,7 @@ class OrdersTableDataStoreTests extends HposTestCase { * @testWith [true] * [false] * - * @param bool $different_request Whether to simulate different requests (as much as we can in a unit test) + * @param bool $different_request Whether to simulate different requests (as much as we can in a unit test). */ public function test_stale_data_is_not_read_sync_off_on( $different_request ) { $this->toggle_cot_authoritative( true ); @@ -2773,7 +2774,7 @@ class OrdersTableDataStoreTests extends HposTestCase { $this->assertEquals( 'test_value', $r_order->get_meta( 'test_key', true ) ); $different_request && $this->reset_order_data_store_state( $cot_store ); - sleep(2); + sleep( 2 ); $this->disable_cot_sync(); $r_order->update_meta_data( 'test_key', 'test_value_updated' ); @@ -2798,8 +2799,8 @@ class OrdersTableDataStoreTests extends HposTestCase { */ private function reset_order_data_store_state( $sut ) { $reset_state = function () use ( $sut ) { - self::$backfilling_order_ids = []; - self::$reading_order_ids = []; + self::$backfilling_order_ids = array(); + self::$reading_order_ids = array(); }; $reset_state->call( $sut ); wp_cache_flush(); @@ -2826,4 +2827,92 @@ class OrdersTableDataStoreTests extends HposTestCase { $this->assertEmpty( $r_order->get_meta_data() ); $this->assertEquals( '', get_post_meta( $order->get_id(), 'test_key', true ) ); } + + /** + * @testDox Test that the protected method get_order_data_for_ids stays backward compatible. + */ + public function test_get_order_data_for_ids_is_back_compat() { + $this->toggle_cot_authoritative( true ); + $this->enable_cot_sync(); + $order = OrderHelper::create_complex_data_store_order( $this->sut ); + $order->set_date_paid( new \WC_DateTime( '2023-01-01 00:00:00' ) ); + $order->set_date_completed( new \WC_DateTime( '2023-01-01 00:00:00' ) ); + $order->set_cart_tax( '1.23' ); + $order->set_customer_id( 1 ); + $order->set_shipping_address_1( 'Line1 Shipping' ); + $order->set_shipping_city( 'City Shipping' ); + $order->set_shipping_postcode( '12345' ); + $order->set_shipping_tax( '12.34' ); + $order->set_shipping_total( '123.45' ); + $order->set_total( '25' ); + $order->set_discount_tax( '2.111' ); + $order->set_discount_total( '1.23' ); + $order->save(); + + $call_protected = function( $ids ) { + return $this->get_order_data_for_ids( $ids ); + }; + + $order_data = $call_protected->call( $this->sut, array( $order->get_id() ) ); + + $expected_data_array = array( + 'id' => $order->get_id(), + 'status' => 'wc-' . $order->get_status(), + 'type' => $order->get_type(), + 'currency' => $order->get_currency(), + 'cart_tax' => '1.23000000', + 'total' => '25.00000000', + 'customer_id' => $order->get_customer_id(), + 'billing_email' => $order->get_billing_email(), + 'date_created' => gmdate( 'Y-m-d H:i:s', $order->get_date_created()->format( 'U' ) ), + 'date_modified' => gmdate( 'Y-m-d H:i:s', $order->get_date_modified()->format( 'U' ) ), + 'parent_id' => $order->get_parent_id(), + 'payment_method' => $order->get_payment_method(), + 'payment_method_title' => $order->get_payment_method_title(), + 'customer_ip_address' => $order->get_customer_ip_address(), + 'transaction_id' => $order->get_transaction_id(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_note' => $order->get_customer_note(), + 'billing_first_name' => $order->get_billing_first_name(), + 'billing_last_name' => $order->get_billing_last_name(), + 'billing_company' => $order->get_billing_company(), + 'billing_address_1' => $order->get_billing_address_1(), + 'billing_address_2' => $order->get_billing_address_2(), + 'billing_city' => $order->get_billing_city(), + 'billing_state' => $order->get_billing_state(), + 'billing_postcode' => $order->get_billing_postcode(), + 'billing_country' => $order->get_billing_country(), + 'billing_phone' => $order->get_billing_phone(), + 'shipping_first_name' => $order->get_shipping_first_name(), + 'shipping_last_name' => $order->get_shipping_last_name(), + 'shipping_company' => $order->get_shipping_company(), + 'shipping_address_1' => $order->get_shipping_address_1(), + 'shipping_address_2' => $order->get_shipping_address_2(), + 'shipping_city' => $order->get_shipping_city(), + 'shipping_state' => $order->get_shipping_state(), + 'shipping_postcode' => $order->get_shipping_postcode(), + 'shipping_country' => $order->get_shipping_country(), + 'shipping_phone' => $order->get_shipping_phone(), + 'created_via' => $order->get_created_via(), + 'version' => $order->get_version(), + 'prices_include_tax' => $order->get_prices_include_tax(), + 'recorded_coupon_usage_counts' => $order->get_recorded_coupon_usage_counts(), + 'download_permissions_granted' => $order->get_download_permissions_granted(), + 'cart_hash' => $order->get_cart_hash(), + 'new_order_email_sent' => $order->get_new_order_email_sent(), + 'order_key' => $order->get_order_key(), + 'order_stock_reduced' => $order->get_order_stock_reduced(), + 'date_paid' => gmdate( 'Y-m-d H:i:s', $order->get_date_paid()->format( 'U' ) ), + 'date_completed' => gmdate( 'Y-m-d H:i:s', $order->get_date_completed()->format( 'U' ) ), + 'shipping_tax' => '12.34000000', + 'shipping_total' => '123.45000000', + 'discount_tax' => '2.11100000', + 'discount_total' => '1.23000000', + 'recorded_sales' => $order->get_recorded_sales(), + ); + + foreach ( $expected_data_array as $key => $value ) { + $this->assertEquals( $value, $order_data[ $order->get_id() ]->{$key}, "Unable to match $key for {$order_data[ $order->get_id() ]->{$key}}. Expected $value" ); + } + } } From 2b5e3f2cb858ed775c5a2d0062be56aa4b2b738a Mon Sep 17 00:00:00 2001 From: Nathan Silveira Date: Mon, 11 Sep 2023 09:41:17 -0300 Subject: [PATCH 25/59] Decode HTML escaped string for tree-item and selected-items components (#40047) --- .../js/components/changelog/fix-category-escape-characters | 5 +++++ .../src/experimental-select-control/selected-items.tsx | 3 ++- .../components/src/experimental-tree-control/tree-item.tsx | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 packages/js/components/changelog/fix-category-escape-characters diff --git a/packages/js/components/changelog/fix-category-escape-characters b/packages/js/components/changelog/fix-category-escape-characters new file mode 100644 index 00000000000..7972320ae66 --- /dev/null +++ b/packages/js/components/changelog/fix-category-escape-characters @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: Decode HTML escaped string for tree-item and selected-items components + + diff --git a/packages/js/components/src/experimental-select-control/selected-items.tsx b/packages/js/components/src/experimental-select-control/selected-items.tsx index 6f82039e640..217d8428a34 100644 --- a/packages/js/components/src/experimental-select-control/selected-items.tsx +++ b/packages/js/components/src/experimental-select-control/selected-items.tsx @@ -3,6 +3,7 @@ */ import classnames from 'classnames'; import { createElement } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -43,7 +44,7 @@ export const SelectedItems = < ItemType, >( {
{ items .map( ( item ) => { - return getItemLabel( item ); + return decodeEntities( getItemLabel( item ) ); } ) .join( ', ' ) }
diff --git a/packages/js/components/src/experimental-tree-control/tree-item.tsx b/packages/js/components/src/experimental-tree-control/tree-item.tsx index d44981b5766..905975a7d2d 100644 --- a/packages/js/components/src/experimental-tree-control/tree-item.tsx +++ b/packages/js/components/src/experimental-tree-control/tree-item.tsx @@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n'; import { chevronDown, chevronUp } from '@wordpress/icons'; import classNames from 'classnames'; import { createElement, forwardRef } from 'react'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -72,7 +73,7 @@ export const TreeItem = forwardRef( function ForwardedTreeItem( { typeof getLabel === 'function' ? ( getLabel( item ) ) : ( - { item.data.label } + { decodeEntities( item.data.label ) } ) } From 39aeb0b37a82c34ee785f7e42f2fe41199bd587e Mon Sep 17 00:00:00 2001 From: Kyle Nel <22053773+kdevnel@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:43:48 +0200 Subject: [PATCH 26/59] Marketplace: Update help menu themes link (#40023) * Update help menu themes link Update plugins/woocommerce/includes/admin/class-wc-admin-help.php Co-authored-by: And Finally * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: And Finally Co-authored-by: github-actions --- .../changelog/update-wccom-17108-help-link-themes | 5 +++++ plugins/woocommerce/includes/admin/class-wc-admin-help.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/update-wccom-17108-help-link-themes diff --git a/plugins/woocommerce/changelog/update-wccom-17108-help-link-themes b/plugins/woocommerce/changelog/update-wccom-17108-help-link-themes new file mode 100644 index 00000000000..019f6d73387 --- /dev/null +++ b/plugins/woocommerce/changelog/update-wccom-17108-help-link-themes @@ -0,0 +1,5 @@ +Significance: patch +Type: tweak +Comment: Updating a URL for themes help link in support menu. + + diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-help.php b/plugins/woocommerce/includes/admin/class-wc-admin-help.php index c5afccda4d3..4bf00e3230e 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-help.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-help.php @@ -76,7 +76,7 @@ class WC_Admin_Help { '

' . __( 'About WooCommerce', 'woocommerce' ) . '

' . '

' . __( 'WordPress.org project', 'woocommerce' ) . '

' . '

' . __( 'GitHub project', 'woocommerce' ) . '

' . - '

' . __( 'Official theme', 'woocommerce' ) . '

' . + '

' . __( 'Official themes', 'woocommerce' ) . '

' . '

' . __( 'Official extensions', 'woocommerce' ) . '

' ); } From 6e20ecb3bcba4555878fac750bb780631daa2ef3 Mon Sep 17 00:00:00 2001 From: Kyle Nel <22053773+kdevnel@users.noreply.github.com> Date: Mon, 11 Sep 2023 15:33:43 +0200 Subject: [PATCH 27/59] Marketplace: update discover page card counts (#40020) * Marketplace: update discover page card counts * Marketplace: Apply PR review feedback * Addressing linter error. Whitespace. * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: And Finally Co-authored-by: github-actions --- .../product-list-content/product-list-content.scss | 11 +++++++++++ .../changelog/fix-wccom-18030-discover-card-count | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-wccom-18030-discover-card-count diff --git a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss index 0161227a6f5..7824d122c8e 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss +++ b/plugins/woocommerce-admin/client/marketplace/components/product-list-content/product-list-content.scss @@ -20,6 +20,11 @@ gap: $large-gap; grid-template-columns: repeat(2, 1fr); } + // Hide third and above product cards on Discover page due to API result count + // These are progressively displayed at larger screen sizes. + &__discover .woocommerce-marketplace__product-card:nth-child(n+3) { + display: none; + } } } @@ -29,6 +34,9 @@ gap: $large-gap; grid-template-columns: repeat(3, 1fr); } + &__discover .woocommerce-marketplace__product-card:nth-child(3) { + display: block; + } } } @@ -37,5 +45,8 @@ &__product-list-content { grid-template-columns: repeat(4, 1fr); } + &__discover .woocommerce-marketplace__product-card:nth-child(4) { + display: block; + } } } diff --git a/plugins/woocommerce/changelog/fix-wccom-18030-discover-card-count b/plugins/woocommerce/changelog/fix-wccom-18030-discover-card-count new file mode 100644 index 00000000000..bda312611c1 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wccom-18030-discover-card-count @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: CSS to render correct number of columns in marketplace Discover screen. + + From 9a75cec842ce801a3c043a135d9c14de69aa7894 Mon Sep 17 00:00:00 2001 From: Moon Date: Mon, 11 Sep 2023 15:14:12 -0700 Subject: [PATCH 28/59] Record plugin api and install failures to coreprofiler_install_plugin_error track (#39899) * Record plugin api and install failures to coreprofiler_install_plugin_error track * Add changefile(s) from automation for the following project(s): woocommerce * Change to trigger GH jobs --------- Co-authored-by: github-actions --- ...lugin-failure-separately-for-core-profiler | 4 ++ .../src/Admin/API/OnboardingPlugins.php | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-39699-track-plugin-failure-separately-for-core-profiler diff --git a/plugins/woocommerce/changelog/update-39699-track-plugin-failure-separately-for-core-profiler b/plugins/woocommerce/changelog/update-39699-track-plugin-failure-separately-for-core-profiler new file mode 100644 index 00000000000..417f520081b --- /dev/null +++ b/plugins/woocommerce/changelog/update-39699-track-plugin-failure-separately-for-core-profiler @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Records plugin API requests and installation errors to coreprofiler_install_plugin_error separately for the core profiler. diff --git a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php index 4a4b5f44c95..e1b0b0f48f0 100644 --- a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php +++ b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php @@ -148,6 +148,9 @@ class OnboardingPlugins extends WC_REST_Data_Controller { true ); } + + add_action( 'woocommerce_plugins_install_error', array( $this, 'log_plugins_install_error' ), 10, 4 ); + add_action( 'woocommerce_plugins_install_api_error', array( $this, 'log_plugins_install_api_error' ), 10, 2 ); } /** @@ -411,4 +414,41 @@ class OnboardingPlugins extends WC_REST_Data_Controller { ), ); } + + public function log_plugins_install_error( $slug, $api, $result, $upgrader ) { + $properties = array( + 'error_message' => sprintf( + /* translators: %s: plugin slug (example: woocommerce-services) */ + __( + 'The requested plugin `%s` could not be installed.', + 'woocommerce' + ), + $slug + ), + 'type' => 'plugin_info_api_error', + 'slug' => $slug, + 'api_version' => $api->version, + 'api_download_link' => $api->download_link, + 'upgrader_skin_message' => implode( ',', $upgrader->skin->get_upgrade_messages() ), + 'result' => is_wp_error( $result ) ? $result->get_error_message() : 'null', + ); + wc_admin_record_tracks_event( 'coreprofiler_install_plugin_error', $properties ); + } + + public function log_plugins_install_api_error( $slug, $api ) { + $properties = array( + 'error_message' => sprintf( + // translators: %s: plugin slug (example: woocommerce-services). + __( + 'The requested plugin `%s` could not be installed. Plugin API call failed.', + 'woocommerce' + ), + $slug + ), + 'type' => 'plugin_install_error', + 'api_error_message' => $api->get_error_message(), + 'slug' => $slug, + ); + wc_admin_record_tracks_event( 'coreprofiler_install_plugin_error', $properties ); + } } From 267cf523949a014f1f79f25be6c432b3793db854 Mon Sep 17 00:00:00 2001 From: RJ <27843274+rjchow@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:30:54 +1000 Subject: [PATCH 29/59] add: customize store design with ai loader (#40083) * add: customize store design with ai loader * removed unused prop --- .../loader-analyzing-your-responses.svg | 43 ++++++++ .../loader-applying-the-finishing-touches.svg | 47 +++++++++ .../loader-assembling-ai-optimized-store.svg | 83 ++++++++++++++++ ...loader-comparing-top-performing-stores.svg | 61 ++++++++++++ .../images/loader-designing-the-best-look.svg | 31 ++++++ .../design-with-ai/pages/ApiCallLoader.tsx | 97 +++++++++++++++++-- .../design-with-ai/stories/ApiCallLoader.tsx | 5 +- .../stories/BusinessInfoDescription.tsx | 8 +- .../design-with-ai/stories/LookAndFeel.tsx | 8 +- .../design-with-ai/stories/ToneOfVoice.tsx | 8 +- .../add-customize-store-design-with-ai-loader | 4 + 11 files changed, 378 insertions(+), 17 deletions(-) create mode 100644 plugins/woocommerce-admin/client/customize-store/assets/images/loader-analyzing-your-responses.svg create mode 100644 plugins/woocommerce-admin/client/customize-store/assets/images/loader-applying-the-finishing-touches.svg create mode 100644 plugins/woocommerce-admin/client/customize-store/assets/images/loader-assembling-ai-optimized-store.svg create mode 100644 plugins/woocommerce-admin/client/customize-store/assets/images/loader-comparing-top-performing-stores.svg create mode 100644 plugins/woocommerce-admin/client/customize-store/assets/images/loader-designing-the-best-look.svg create mode 100644 plugins/woocommerce/changelog/add-customize-store-design-with-ai-loader diff --git a/plugins/woocommerce-admin/client/customize-store/assets/images/loader-analyzing-your-responses.svg b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-analyzing-your-responses.svg new file mode 100644 index 00000000000..1294b344729 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-analyzing-your-responses.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/assets/images/loader-applying-the-finishing-touches.svg b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-applying-the-finishing-touches.svg new file mode 100644 index 00000000000..08d2a648b27 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-applying-the-finishing-touches.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/assets/images/loader-assembling-ai-optimized-store.svg b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-assembling-ai-optimized-store.svg new file mode 100644 index 00000000000..9586796e1f6 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-assembling-ai-optimized-store.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/assets/images/loader-comparing-top-performing-stores.svg b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-comparing-top-performing-stores.svg new file mode 100644 index 00000000000..12e2ea7eb6f --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-comparing-top-performing-stores.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/assets/images/loader-designing-the-best-look.svg b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-designing-the-best-look.svg new file mode 100644 index 00000000000..6110d2d5fa9 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assets/images/loader-designing-the-best-look.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ApiCallLoader.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ApiCallLoader.tsx index b7dacb481f8..78344829c13 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ApiCallLoader.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/pages/ApiCallLoader.tsx @@ -1,17 +1,94 @@ +/** + * External dependencies + */ +import { Loader } from '@woocommerce/onboarding'; +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ -import { designWithAiStateMachineContext } from '../types'; +import analyzingYourResponses from '../../assets/images/loader-analyzing-your-responses.svg'; +import designingTheBestLook from '../../assets/images/loader-designing-the-best-look.svg'; +import comparingTheTopPerformingStores from '../../assets/images/loader-comparing-top-performing-stores.svg'; +import assemblingAiOptimizedStore from '../../assets/images/loader-assembling-ai-optimized-store.svg'; +import applyingFinishingTouches from '../../assets/images/loader-applying-the-finishing-touches.svg'; -export const ApiCallLoader = ( { - context, -}: { - context: designWithAiStateMachineContext; -} ) => { +const loaderSteps = [ + { + title: __( 'Analyzing your responses', 'woocommerce' ), + image: ( + { + ), + progress: 17, + }, + { + title: __( 'Comparing the top performing stores', 'woocommerce' ), + image: ( + { + ), + progress: 33, + }, + { + title: __( 'Designing the best look for your business', 'woocommerce' ), + image: ( + { + ), + progress: 50, + }, + { + title: __( 'Assembling your AI-optimized store', 'woocommerce' ), + image: ( + { + ), + progress: 66, + }, + { + title: __( 'Applying the finishing touches', 'woocommerce' ), + image: ( + { + ), + progress: 83, + }, +]; + +export const ApiCallLoader = () => { return ( -
-

Loader

-
{ JSON.stringify( context ) }
-
+ + + { loaderSteps.map( ( step, index ) => ( + + + { step.image } + + { step.title } + + + ) ) } + + ); }; diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ApiCallLoader.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ApiCallLoader.tsx index 9abbf9c83e0..35beb19868a 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ApiCallLoader.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ApiCallLoader.tsx @@ -1,13 +1,10 @@ /** * Internal dependencies */ -import { designWithAiStateMachineContext } from '../types'; import { ApiCallLoader } from '../pages'; import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout'; -export const ApiCallLoaderPage = () => ( - -); +export const ApiCallLoaderPage = () => ; export default { title: 'WooCommerce Admin/Application/Customize Store/Design with AI/API Call Loader', diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/BusinessInfoDescription.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/BusinessInfoDescription.tsx index 524fa11f840..822ab736de6 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/BusinessInfoDescription.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/BusinessInfoDescription.tsx @@ -7,7 +7,13 @@ import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout'; export const BusinessInfoDescriptionPage = () => ( {} } /> ); diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/LookAndFeel.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/LookAndFeel.tsx index 8f1847dc071..6137fe1eb46 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/LookAndFeel.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/LookAndFeel.tsx @@ -7,7 +7,13 @@ import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout'; export const LookAndFeelPage = () => ( {} } /> ); diff --git a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ToneOfVoice.tsx b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ToneOfVoice.tsx index 11c32cfdf44..7b904ed1883 100644 --- a/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ToneOfVoice.tsx +++ b/plugins/woocommerce-admin/client/customize-store/design-with-ai/stories/ToneOfVoice.tsx @@ -7,7 +7,13 @@ import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout'; export const ToneOfVoicePage = () => ( {} } /> ); diff --git a/plugins/woocommerce/changelog/add-customize-store-design-with-ai-loader b/plugins/woocommerce/changelog/add-customize-store-design-with-ai-loader new file mode 100644 index 00000000000..e5c08aa4d81 --- /dev/null +++ b/plugins/woocommerce/changelog/add-customize-store-design-with-ai-loader @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Implemented loader design for Customize your store - Design with AI From c72115912982b9fec4e7a0d504b1ab8ef5989c91 Mon Sep 17 00:00:00 2001 From: Paul Sealock Date: Tue, 12 Sep 2023 15:13:42 +1200 Subject: [PATCH 30/59] Remote Inbox Notifications: Add `in` and `!in` comparison operators (#40084) --- .../changelog/add-RIN-in-not-in-comparison | 4 ++++ .../ComparisonOperation.php | 10 ++++++++++ .../src/Admin/RemoteInboxNotifications/README.md | 16 ++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 plugins/woocommerce/changelog/add-RIN-in-not-in-comparison diff --git a/plugins/woocommerce/changelog/add-RIN-in-not-in-comparison b/plugins/woocommerce/changelog/add-RIN-in-not-in-comparison new file mode 100644 index 00000000000..d6f9ab2c518 --- /dev/null +++ b/plugins/woocommerce/changelog/add-RIN-in-not-in-comparison @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update Remote Inbox Notifications to add in and !in comparison operators for comparing values against arrays diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/ComparisonOperation.php b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/ComparisonOperation.php index 8be740a8a10..82caffb187d 100644 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/ComparisonOperation.php +++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/ComparisonOperation.php @@ -42,6 +42,16 @@ class ComparisonOperation { return ! in_array( $right_operand, $left_operand, true ); } return strpos( $right_operand, $left_operand ) === false; + case 'in': + if ( is_array( $right_operand ) && is_string( $left_operand ) ) { + return in_array( $left_operand, $right_operand, true ); + } + return strpos( $left_operand, $right_operand ) !== false; + case '!in': + if ( is_array( $right_operand ) && is_string( $left_operand ) ) { + return ! in_array( $left_operand, $right_operand, true ); + } + return strpos( $left_operand, $right_operand ) === false; } return false; diff --git a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md index 1214c705813..e2063fe95d2 100644 --- a/plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md +++ b/plugins/woocommerce/src/Admin/RemoteInboxNotifications/README.md @@ -151,6 +151,8 @@ values. The following operations are implemented: - `!=` - `contains` - `!contains` +- `in` (Added in WooCommerce 8.2.0) +- `!in` (Added in WooCommerce 8.2.0) `contains` and `!contains` allow checking if the provided value is present (or not present) in the haystack value. An example of this is using the @@ -167,6 +169,20 @@ onboarding profile: } ``` +`in` and `!in` allow checking if a value is found (or not found) in a provided array. For example, using the `in` comparison operator to check if the base country location value is found in a given array, as below. This rule matches if the `base_location_country` is `US`, `NZ`, or `ZA`. **NOTE:** These comparisons were added in **WooCommerce 8.2.0**. If the spec is read by an older version of WooCommerce, the rule will evaluate to `false`. + +```json +{ + "type": "base_location_country", + "value": [ + "US", + "NZ", + "ZA" + ], + "operation": "in" +} +``` + ### Plugins activated This passes if all of the listed plugins are installed and activated. From e52d11a87ef3b3bd76540e0c94145369a4711c9c Mon Sep 17 00:00:00 2001 From: RJ <27843274+rjchow@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:32:50 +1000 Subject: [PATCH 31/59] add: url navigation to cys (#40068) * add: url navigation to cys * bugfix for url not updating * url handling for design-with-ai * fixed url syncing so that it's working with @woocommerce/navigation * changed useLocation to useQuery in assembler-hub save-hub --- .../assembler-hub/sidebar/index.tsx | 57 +++++---- .../assembler-hub/sidebar/save-hub.tsx | 15 +-- .../sidebar-navigation-screen-main.tsx | 14 +-- .../customize-store/design-with-ai/actions.ts | 25 ++++ .../design-with-ai/services.ts | 13 ++ .../design-with-ai/state-machine.tsx | 89 ++++++++++++- .../customize-store/design-with-ai/types.ts | 5 +- .../client/customize-store/index.tsx | 119 +++++++++++++++--- .../changelog/add-cys-url-navigation | 4 + 9 files changed, 275 insertions(+), 66 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-cys-url-navigation diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx index a99de4b0433..2478b41814b 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/index.tsx @@ -13,10 +13,6 @@ import { // @ts-ignore No types for this exist yet. __experimentalUseNavigator as useNavigator, } from '@wordpress/components'; -// @ts-ignore No types for this exist yet. -import { privateApis as routerPrivateApis } from '@wordpress/router'; -// @ts-ignore No types for this exist yet. -import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; /** * Internal dependencies @@ -31,8 +27,12 @@ import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages' import { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo'; import { SaveHub } from './save-hub'; - -const { useLocation, useHistory } = unlock( routerPrivateApis ); +import { + addHistoryListener, + getQuery, + updateQueryString, + useQuery, +} from '@woocommerce/navigation'; function isSubset( subset: { @@ -48,18 +48,27 @@ function isSubset( } function useSyncPathWithURL() { - const history = useHistory(); - const { params: urlParams } = useLocation(); - const { location: navigatorLocation, params: navigatorParams } = - useNavigator(); + const urlParams = useQuery(); + const { + location: navigatorLocation, + params: navigatorParams, + goTo, + } = useNavigator(); const isMounting = useRef( true ); useEffect( () => { - // The navigatorParams are only initially filled properly when the - // navigator screens mount. so we ignore the first synchronisation. + // The navigatorParams are only initially filled properly after the + // navigator screens mounts. so we don't do the query string update initially. + // however we also do want to add an event listener for popstate so that we can + // update the navigator when the user navigates using the browser back button if ( isMounting.current ) { isMounting.current = false; + addHistoryListener( ( event: PopStateEvent ) => { + if ( event.type === 'popstate' ) { + goTo( ( getQuery() as Record< string, string > ).path ); + } + } ); return; } @@ -73,7 +82,7 @@ function useSyncPathWithURL() { ...urlParams, ...newUrlParams, }; - history.push( updatedParams ); + updateQueryString( {}, updatedParams.path ); } updateUrlParams( { @@ -97,28 +106,28 @@ function SidebarScreens() { useSyncPathWithURL(); return ( <> - + - + - + - + - + - + - + - + @@ -126,8 +135,10 @@ function SidebarScreens() { } function Sidebar() { - const { params: urlParams } = useLocation(); - const initialPath = useRef( urlParams.path ?? '/customize-store' ); + const urlParams = getQuery() as Record< string, string >; + const initialPath = useRef( + urlParams.path ?? '/customize-store/assembler-hub' + ); return ( <> { const saveNoticeId = 'site-edit-save-notice'; - const { params } = useLocation(); + const urlParams = useQuery(); const { sendEvent } = useContext( CustomizeStoreContext ); // @ts-ignore No types for this exist yet. @@ -141,7 +134,7 @@ export const SaveHub = () => { } ); // Only run when path changes. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ params.path ] ); + }, [ urlParams.path ] ); const save = async () => { removeNotice( saveNoticeId ); @@ -182,7 +175,7 @@ export const SaveHub = () => { }; const renderButton = () => { - if ( params.path === '/customize-store' ) { + if ( urlParams.path === '/customize-store/assembler-hub' ) { return ( + ); +}; diff --git a/plugins/woo-ai/src/components/random-loading-message/random-loading-message.tsx b/plugins/woo-ai/src/components/random-loading-message/random-loading-message.tsx index b29ca9e304d..b10c8d9de99 100644 --- a/plugins/woo-ai/src/components/random-loading-message/random-loading-message.tsx +++ b/plugins/woo-ai/src/components/random-loading-message/random-loading-message.tsx @@ -131,7 +131,9 @@ export const RandomLoadingMessage: React.FC< RandomLoadingMessageProps > = ( { - { currentMessage } + + { currentMessage } + ); }; diff --git a/plugins/woo-ai/src/components/suggestion-pills/index.ts b/plugins/woo-ai/src/components/suggestion-pills/index.ts new file mode 100644 index 00000000000..e200d53cd4f --- /dev/null +++ b/plugins/woo-ai/src/components/suggestion-pills/index.ts @@ -0,0 +1 @@ +export * from './suggestion-pills'; diff --git a/plugins/woo-ai/src/components/suggestion-pills/suggestion-pill-item.tsx b/plugins/woo-ai/src/components/suggestion-pills/suggestion-pill-item.tsx new file mode 100644 index 00000000000..bf031f0c983 --- /dev/null +++ b/plugins/woo-ai/src/components/suggestion-pills/suggestion-pill-item.tsx @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import React from 'react'; + +type SuggestionPillItemProps = { + suggestion: string; + onSuggestionClick: ( suggestion: string ) => void; +}; + +export const SuggestionPillItem: React.FC< SuggestionPillItemProps > = ( { + suggestion, + onSuggestionClick, +} ) => ( +
  • + +
  • +); diff --git a/plugins/woo-ai/src/components/suggestion-pills/suggestion-pills.scss b/plugins/woo-ai/src/components/suggestion-pills/suggestion-pills.scss new file mode 100644 index 00000000000..702d6d58959 --- /dev/null +++ b/plugins/woo-ai/src/components/suggestion-pills/suggestion-pills.scss @@ -0,0 +1,21 @@ +.woo-ai-suggestion-pills { + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; + + &__item { + margin-bottom: 0; + } + + &__select-suggestion.button { + border-radius: 28px; + min-width: 0; + word-wrap: break-word; + overflow-wrap: anywhere; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + } +} diff --git a/plugins/woo-ai/src/components/suggestion-pills/suggestion-pills.tsx b/plugins/woo-ai/src/components/suggestion-pills/suggestion-pills.tsx new file mode 100644 index 00000000000..694f86fd044 --- /dev/null +++ b/plugins/woo-ai/src/components/suggestion-pills/suggestion-pills.tsx @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import React from 'react'; + +/** + * Internal dependencies + */ +import { SuggestionPillItem } from './suggestion-pill-item'; + +type SuggestionPillsProps = { + suggestions: string[]; + onSuggestionClick: ( suggestion: string ) => void; +}; + +export const SuggestionPills: React.FC< SuggestionPillsProps > = ( { + suggestions, + onSuggestionClick, +} ) => ( +
      + { suggestions.map( ( suggestion, index ) => ( + onSuggestionClick( suggestion ) } + /> + ) ) } +
    +); diff --git a/plugins/woo-ai/src/constants.ts b/plugins/woo-ai/src/constants.ts index dddeeca962a..647e9c58ae7 100644 --- a/plugins/woo-ai/src/constants.ts +++ b/plugins/woo-ai/src/constants.ts @@ -2,3 +2,4 @@ export const WOO_AI_PLUGIN_FEATURE_NAME = 'woo_ai_plugin'; export const MAX_TITLE_LENGTH = 200; export const MIN_TITLE_LENGTH_FOR_DESCRIPTION = 15; export const MIN_DESC_LENGTH_FOR_SHORT_DESC = 100; +export const DESCRIPTION_MAX_LENGTH = 300; diff --git a/plugins/woo-ai/src/index.js b/plugins/woo-ai/src/index.js index a16d2d3ecd5..1a284456eca 100644 --- a/plugins/woo-ai/src/index.js +++ b/plugins/woo-ai/src/index.js @@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from 'react-query'; */ import { WriteItForMeButtonContainer } from './product-description'; import { ProductNameSuggestions } from './product-name'; +import { ProductCategorySuggestions } from './product-category'; import { WriteShortDescriptionButtonContainer } from './product-short-description'; import setPreferencesPersistence from './utils/preferencesPersistence'; @@ -37,6 +38,16 @@ const renderComponent = ( Component, rootElement ) => { } }; +const renderProductCategorySuggestions = () => { + const root = document.createElement( 'div' ); + root.id = 'woocommerce-ai-app-product-category-suggestions'; + + renderComponent( ProductCategorySuggestions, root ); + + // Insert the category suggestions node in the product category meta box. + document.getElementById( 'taxonomy-product_cat' ).append( root ); +}; + const descriptionButtonRoot = document.getElementById( 'woocommerce-ai-app-product-gpt-button' ); @@ -51,6 +62,7 @@ const shortDescriptionButtonRoot = document.getElementById( if ( window.JP_CONNECTION_INITIAL_STATE?.connectionStatus?.isActive ) { renderComponent( WriteItForMeButtonContainer, descriptionButtonRoot ); renderComponent( ProductNameSuggestions, nameSuggestionsRoot ); + renderProductCategorySuggestions(); renderComponent( WriteShortDescriptionButtonContainer, shortDescriptionButtonRoot diff --git a/plugins/woo-ai/src/index.scss b/plugins/woo-ai/src/index.scss index 83c8aa2df71..2e6fffee33f 100644 --- a/plugins/woo-ai/src/index.scss +++ b/plugins/woo-ai/src/index.scss @@ -1,3 +1,5 @@ -@import 'product-description/product-description.scss'; -@import 'product-name/product-name.scss'; +@import "components"; +@import "product-description/product-description.scss"; +@import "product-name/product-name.scss"; +@import 'product-category/product-category.scss'; @import 'product-short-description/product-short-description.scss'; diff --git a/plugins/woo-ai/src/product-category/category-suggestion-feedback.scss b/plugins/woo-ai/src/product-category/category-suggestion-feedback.scss new file mode 100644 index 00000000000..4f1ba120d32 --- /dev/null +++ b/plugins/woo-ai/src/product-category/category-suggestion-feedback.scss @@ -0,0 +1,19 @@ +.category-suggestions-feedback { + .notice { + position: relative; + + span { + vertical-align: middle; + } + + .button-group { + .button { + background: initial; + border: none; + } + .button:hover { + text-decoration: underline; + } + } + } +} diff --git a/plugins/woo-ai/src/product-category/category-suggestion-feedback.tsx b/plugins/woo-ai/src/product-category/category-suggestion-feedback.tsx new file mode 100644 index 00000000000..4a9facb177d --- /dev/null +++ b/plugins/woo-ai/src/product-category/category-suggestion-feedback.tsx @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { recordCategoryTracks } from './utils'; + +export const CategorySuggestionFeedback = () => { + const [ hide, setHide ] = useState( false ); + + const submitFeedback = ( positive: boolean ) => { + setHide( true ); + recordCategoryTracks( 'feedback', { + response: positive ? 'positive' : 'negative', + } ); + }; + + return ( +
    + { ! hide && ( +
    + { __( 'How did we do?', 'woocommerce' ) } + + + + +
    + ) } +
    + ); +}; diff --git a/plugins/woo-ai/src/product-category/index.ts b/plugins/woo-ai/src/product-category/index.ts new file mode 100644 index 00000000000..54e07d06cd2 --- /dev/null +++ b/plugins/woo-ai/src/product-category/index.ts @@ -0,0 +1 @@ +export * from './product-category-suggestions'; diff --git a/plugins/woo-ai/src/product-category/product-category-suggestions.tsx b/plugins/woo-ai/src/product-category/product-category-suggestions.tsx new file mode 100644 index 00000000000..772a3e81068 --- /dev/null +++ b/plugins/woo-ai/src/product-category/product-category-suggestions.tsx @@ -0,0 +1,372 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useCallback, useEffect, useState } from '@wordpress/element'; +import { UseCompletionError } from '@woocommerce/ai'; + +/** + * Internal dependencies + */ +import { MagicButton, RandomLoadingMessage } from '../components'; +import { getCategories, selectCategory } from '../utils'; +import AlertIcon from '../../assets/images/icons/alert.svg'; +import { getAvailableCategoryPaths, recordCategoryTracks } from './utils'; +import { useNewCategorySuggestions } from './useNewCategorySuggestions'; +import { useExistingCategorySuggestions } from './useExistingCategorySuggestions'; +import { createCategoriesFromPath } from '../utils/categoryCreator'; +import { CategorySuggestionFeedback } from './category-suggestion-feedback'; + +enum SuggestionsState { + Initial, + Fetching, + Failed, + Complete, + None, +} + +export const ProductCategorySuggestions = () => { + const [ existingSuggestionsState, setExistingSuggestionsState ] = + useState< SuggestionsState >( SuggestionsState.Initial ); + const [ newSuggestionsState, setNewSuggestionsState ] = + useState< SuggestionsState >( SuggestionsState.Initial ); + const [ existingSuggestions, setExistingSuggestions ] = useState< + string[] + >( [] ); + const [ newSuggestions, setNewSuggestions ] = useState< string[] >( [] ); + const [ showFeedback, setShowFeedback ] = useState( false ); + let feedbackTimeout: number | null = null; + + useEffect( () => { + recordCategoryTracks( 'view_ui' ); + }, [] ); + + /** + * Show the feedback box after a delay. + */ + const showFeedbackAfterDelay = () => { + if ( feedbackTimeout ) { + clearTimeout( feedbackTimeout ); + feedbackTimeout = null; + } + + feedbackTimeout = setTimeout( () => { + setShowFeedback( true ); + }, 5000 ); + }; + + /** + * Reset the feedback box. + */ + const resetFeedbackBox = () => { + if ( feedbackTimeout ) { + clearTimeout( feedbackTimeout ); + feedbackTimeout = null; + } + setShowFeedback( false ); + }; + + /** + * Check if a suggestion is valid. + * + * @param suggestion The suggestion to check. + * @param selectedCategories The currently selected categories. + */ + const isSuggestionValid = ( + suggestion: string, + selectedCategories: string[] + ) => { + return ( + suggestion !== __( 'Uncategorized', 'woocommerce' ) && + ! selectedCategories.includes( suggestion ) + ); + }; + + /** + * Callback for when the existing category suggestions have been generated. + * + * @param {string[]} existingCategorySuggestions The existing category suggestions. + */ + const onExistingCategorySuggestionsGenerated = async ( + existingCategorySuggestions: string[] + ) => { + let filtered: string[] = []; + try { + const availableCategories = await getAvailableCategoryPaths(); + + // Only show suggestions that are valid, available, and not already in the list of new suggestions. + filtered = existingCategorySuggestions.filter( + ( suggestion ) => + isSuggestionValid( suggestion, getCategories() ) && + availableCategories.includes( suggestion ) && + ! newSuggestions.includes( suggestion ) + ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.error( 'Unable to fetch available categories.', e ); + } + + if ( filtered.length === 0 ) { + setExistingSuggestionsState( SuggestionsState.None ); + } else { + setExistingSuggestionsState( SuggestionsState.Complete ); + } + setExistingSuggestions( filtered ); + + showFeedbackAfterDelay(); + recordCategoryTracks( 'stop', { + reason: 'finished', + suggestions_type: 'existing', + suggestions: existingCategorySuggestions, + valid_suggestions: filtered, + } ); + }; + + /** + * Callback for when the new category suggestions have been generated. + * + * @param {string[]} newCategorySuggestions + */ + const onNewCategorySuggestionsGenerated = async ( + newCategorySuggestions: string[] + ) => { + let filtered: string[] = []; + try { + const availableCategories = await getAvailableCategoryPaths(); + + // Only show suggestions that are valid, NOT already available, and not already in the list of existing suggestions. + filtered = newCategorySuggestions.filter( + ( suggestion ) => + isSuggestionValid( suggestion, getCategories() ) && + ! availableCategories.includes( suggestion ) && + ! existingSuggestions.includes( suggestion ) + ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.error( 'Unable to fetch available categories.', e ); + } + + if ( filtered.length === 0 ) { + setNewSuggestionsState( SuggestionsState.None ); + } else { + setNewSuggestionsState( SuggestionsState.Complete ); + } + setNewSuggestions( filtered ); + + showFeedbackAfterDelay(); + recordCategoryTracks( 'stop', { + reason: 'finished', + suggestions_type: 'new', + suggestions: newCategorySuggestions, + valid_suggestions: filtered, + } ); + }; + + /** + * Callback for when an error occurs while generating the existing category suggestions. + * + * @param {UseCompletionError} error + */ + const onExistingCatSuggestionError = ( error: UseCompletionError ) => { + // eslint-disable-next-line no-console + console.debug( 'Streaming error encountered', error ); + recordCategoryTracks( 'stop', { + reason: 'error', + suggestions_type: 'existing', + error: error.code ?? error.message, + } ); + setExistingSuggestionsState( SuggestionsState.Failed ); + }; + + /** + * Callback for when an error occurs while generating the new category suggestions. + * + * @param {UseCompletionError} error + */ + const onNewCatSuggestionError = ( error: UseCompletionError ) => { + // eslint-disable-next-line no-console + console.debug( 'Streaming error encountered', error ); + recordCategoryTracks( 'stop', { + reason: 'error', + suggestions_type: 'new', + error: error.code ?? error.message, + } ); + setNewSuggestionsState( SuggestionsState.Failed ); + }; + + const { fetchSuggestions: fetchExistingCategorySuggestions } = + useExistingCategorySuggestions( + onExistingCategorySuggestionsGenerated, + onExistingCatSuggestionError + ); + + const { fetchSuggestions: fetchNewCategorySuggestions } = + useNewCategorySuggestions( + onNewCategorySuggestionsGenerated, + onNewCatSuggestionError + ); + + /** + * Callback for when an existing category suggestion is clicked. + * + * @param {string} suggestion The suggestion that was clicked. + */ + const handleExistingSuggestionClick = useCallback( + ( suggestion: string ) => { + // remove the selected item from the list of suggestions + setExistingSuggestions( + existingSuggestions.filter( ( s ) => s !== suggestion ) + ); + selectCategory( suggestion ); + + recordCategoryTracks( 'select', { + selected_category: suggestion, + suggestions_type: 'existing', + } ); + }, + [ existingSuggestions ] + ); + + /** + * Callback for when a new category suggestion is clicked. + * + * @param {string} suggestion The suggestion that was clicked. + */ + const handleNewSuggestionClick = useCallback( + async ( suggestion: string ) => { + // remove the selected item from the list of suggestions + setNewSuggestions( + newSuggestions.filter( ( s ) => s !== suggestion ) + ); + + try { + await createCategoriesFromPath( suggestion ); + + recordCategoryTracks( 'select', { + selected_category: suggestion, + suggestions_type: 'new', + } ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.error( 'Unable to create category', e ); + } + }, + [ newSuggestions ] + ); + + const fetchProductSuggestions = async () => { + resetFeedbackBox(); + setExistingSuggestions( [] ); + setNewSuggestions( [] ); + setExistingSuggestionsState( SuggestionsState.Fetching ); + setNewSuggestionsState( SuggestionsState.Fetching ); + + recordCategoryTracks( 'start', { + current_categories: getCategories(), + } ); + + await Promise.all( [ + fetchExistingCategorySuggestions(), + fetchNewCategorySuggestions(), + ] ); + }; + + return ( +
    + + { ( existingSuggestionsState === SuggestionsState.Fetching || + newSuggestionsState === SuggestionsState.Fetching ) && ( +
    +

    + +

    +
    + ) } + { existingSuggestionsState === SuggestionsState.None && + newSuggestionsState === SuggestionsState.None && ( +
    +

    + { __( + 'Unable to generate a matching category for the product. Please try including more information about the product in the title and description.', + 'woocommerce' + ) } +

    +
    + ) } + { existingSuggestionsState === SuggestionsState.Failed && + newSuggestionsState === SuggestionsState.Failed && ( +
    +

    + + { __( + `We're currently experiencing high demand for our experimental feature. Please check back in shortly!`, + 'woocommerce' + ) } +

    +
    + ) } + { ( existingSuggestionsState === SuggestionsState.Complete || + newSuggestionsState === SuggestionsState.Complete ) && ( +
    +
      + { existingSuggestions.map( ( suggestion ) => ( +
    • + +
    • + ) ) } + { newSuggestions.map( ( suggestion ) => ( +
    • + +
    • + ) ) } +
    + { showFeedback && ( +
    + +
    + ) } +
    + ) } +
    + ); +}; diff --git a/plugins/woo-ai/src/product-category/product-category.scss b/plugins/woo-ai/src/product-category/product-category.scss new file mode 100644 index 00000000000..af108b0f977 --- /dev/null +++ b/plugins/woo-ai/src/product-category/product-category.scss @@ -0,0 +1,51 @@ +@import "category-suggestion-feedback"; +.wc-product-category-suggestions { + &__loading-message { + display: flex; + align-items: center; + + .woo-ai-loading-message_spinner { + display: flex; + + .woocommerce-spinner { + width: 24px; + height: 24px; + min-width: 24px; + max-height: 24px; + } + } + + .woo-ai-loading-message_content { + margin-left: 5px; + } + } + + .woo-ai-write-it-for-me-btn { + display: flex; + align-items: center; + img { + filter: invert(32%) sepia(36%) saturate(2913%) hue-rotate(161deg) brightness(87%) contrast(91%); + } + } + + &__suggestions { + margin-top: 10px; + margin-bottom: 0; + + li:last-child { + margin-bottom: 0; + } + } + + .woo-ai-write-it-for-me-btn { + margin: 0; + width: 100%; + display: flex; + justify-content: center; + } +} + +.woocommerce-embed-page #wpbody-content .wc-product-category-suggestions .notice { + margin-top: 10px; + margin-bottom: 0; +} diff --git a/plugins/woo-ai/src/product-category/useExistingCategorySuggestions.ts b/plugins/woo-ai/src/product-category/useExistingCategorySuggestions.ts new file mode 100644 index 00000000000..e5da3956e13 --- /dev/null +++ b/plugins/woo-ai/src/product-category/useExistingCategorySuggestions.ts @@ -0,0 +1,96 @@ +/** + * External dependencies + */ +import { + __experimentalUseCompletion as useCompletion, + UseCompletionError, +} from '@woocommerce/ai'; + +/** + * Internal dependencies + */ +import { WOO_AI_PLUGIN_FEATURE_NAME } from '../constants'; +import { generateProductDataInstructions, ProductProps } from '../utils'; +import { getAvailableCategoryPaths } from './utils'; + +type UseExistingCategorySuggestionsHook = { + fetchSuggestions: () => Promise< void >; +}; + +export const useExistingCategorySuggestions = ( + onSuggestionsGenerated: ( suggestions: string[] ) => void, + onError: ( error: UseCompletionError ) => void +): UseExistingCategorySuggestionsHook => { + const { requestCompletion } = useCompletion( { + feature: WOO_AI_PLUGIN_FEATURE_NAME, + onStreamError: ( error: UseCompletionError ) => { + // eslint-disable-next-line no-console + console.debug( 'Streaming error encountered', error ); + + onError( error ); + }, + onCompletionFinished: async ( reason, content ) => { + if ( reason === 'error' ) { + throw Error( 'Invalid response' ); + } + if ( ! content ) { + throw Error( 'No suggestions were generated' ); + } + + try { + const parsed = content + .split( ',' ) + .map( ( suggestion ) => { + return suggestion.trim(); + } ) + .filter( Boolean ); + + onSuggestionsGenerated( parsed ); + } catch ( e ) { + throw Error( 'Unable to parse suggestions' ); + } + }, + } ); + + const buildPrompt = async () => { + let availableCategories: string[] = []; + try { + availableCategories = await getAvailableCategoryPaths(); + } catch ( e ) { + // eslint-disable-next-line no-console + console.error( 'Unable to fetch available categories', e ); + } + + const productPropsInstructions = generateProductDataInstructions( { + excludeProps: [ ProductProps.Categories ], + } ); + const instructions = [ + 'You are a WooCommerce SEO and marketing expert.', + `Using the product's ${ productPropsInstructions.includedProps.join( + ', ' + ) } suggest only one category that best matches the product.`, + 'Categories can have parents and multi-level children structures like Parent Category > Child Category.', + availableCategories + ? `You will be given a list of available categories. Find the best matching category from this list. Available categories are: ${ availableCategories.join( + ', ' + ) }` + : '', + "The product's properties are:", + ...productPropsInstructions.instructions, + 'Return only one product category, children categories must be separated by >.', + 'Here is an example of a valid response:', + 'Parent Category > Subcategory > Another Subcategory', + 'Do not output the example response. Respond only with the suggested categories. Do not say anything else.', + ]; + + return instructions.join( '\n' ); + }; + + const fetchSuggestions = async () => { + await requestCompletion( await buildPrompt() ); + }; + + return { + fetchSuggestions, + }; +}; diff --git a/plugins/woo-ai/src/product-category/useNewCategorySuggestions.ts b/plugins/woo-ai/src/product-category/useNewCategorySuggestions.ts new file mode 100644 index 00000000000..9f3ed829f72 --- /dev/null +++ b/plugins/woo-ai/src/product-category/useNewCategorySuggestions.ts @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import { + __experimentalUseCompletion as useCompletion, + UseCompletionError, +} from '@woocommerce/ai'; + +/** + * Internal dependencies + */ +import { WOO_AI_PLUGIN_FEATURE_NAME } from '../constants'; +import { generateProductDataInstructions, ProductProps } from '../utils'; + +type UseNewCategorySuggestionsHook = { + fetchSuggestions: () => Promise< void >; +}; + +export const useNewCategorySuggestions = ( + onSuggestionsGenerated: ( suggestions: string[] ) => void, + onError: ( error: UseCompletionError ) => void +): UseNewCategorySuggestionsHook => { + const { requestCompletion } = useCompletion( { + feature: WOO_AI_PLUGIN_FEATURE_NAME, + onStreamError: ( error: UseCompletionError ) => { + // eslint-disable-next-line no-console + console.debug( 'Streaming error encountered', error ); + + onError( error ); + }, + onCompletionFinished: async ( reason, content ) => { + if ( reason === 'error' ) { + throw Error( 'Unable to parse suggestions' ); + } + if ( ! content ) { + throw Error( 'No suggestions were generated' ); + } + + try { + const parsed = content + .split( ',' ) + .map( ( suggestion ) => { + return suggestion.trim(); + } ) + .filter( Boolean ); + + onSuggestionsGenerated( parsed ); + } catch ( e ) { + throw Error( 'Unable to parse suggestions' ); + } + }, + } ); + + const buildPrompt = async () => { + const productPropsInstructions = generateProductDataInstructions( { + excludeProps: [ ProductProps.Categories ], + } ); + const instructions = [ + 'You are a WooCommerce SEO and marketing expert.', + `Using the product's ${ productPropsInstructions.includedProps.join( + ', ' + ) } suggest the best matching category from the Google standard product category taxonomy hierarchy.`, + 'The category can optionally have multi-level children structures like Parent Category > Child Category.', + "The product's properties are:", + ...productPropsInstructions.instructions, + 'Return only one best matching product category, children categories must be separated by >.', + 'Here is an example of a valid response:', + 'Parent Category > Child Category > Child of Child Category', + 'Do not output the example response. Respond only with the one suggested category. Do not say anything else.', + ]; + + return instructions.join( '\n' ); + }; + + const fetchSuggestions = async () => { + await requestCompletion( await buildPrompt() ); + }; + + return { + fetchSuggestions, + }; +}; diff --git a/plugins/woo-ai/src/product-category/utils.ts b/plugins/woo-ai/src/product-category/utils.ts new file mode 100644 index 00000000000..f25abccbbc4 --- /dev/null +++ b/plugins/woo-ai/src/product-category/utils.ts @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { getPostId, recordTracksFactory, decodeHtmlEntities } from '../utils'; + +type TracksData = Record< string, string | number | null | Array< string > >; + +type CategoryProps = { + id: number; + name: string; + parent: number; +}; +type CategoriesApiResponse = CategoryProps[]; + +export const recordCategoryTracks = recordTracksFactory< TracksData >( + 'category_completion', + () => ( { + post_id: getPostId(), + } ) +); + +/** + * Get all available categories in the store. + * + * @return {string[]} Array of categories. + * @throws {Error} If the API request fails. + */ +export const getAvailableCategories = + async (): Promise< CategoriesApiResponse > => { + const results = await apiFetch< CategoriesApiResponse >( { + path: '/wc/v3/products/categories?per_page=100&fields=id,name,parent', + } ); + + results.forEach( ( category ) => { + category.name = decodeHtmlEntities( category.name ); + } ); + + return results; + }; + +/** + * Get all available categories in the store as a hierarchical list of strings. + * + * @return {string[]} Array of category names in hierarchical manner where each parent category is separated by a > character. e.g. "Clothing > Shirts > T-Shirts" + * @throws {Error} If the API request fails. + */ +export const getAvailableCategoryPaths = async (): Promise< string[] > => { + const categories: CategoriesApiResponse = await getAvailableCategories(); + + // Create a map of categories by ID + const categoryNamesById: Record< number, CategoryProps > = + categories.reduce( + ( acc, category ) => ( { + ...acc, + [ category.id ]: category, + } ), + {} + ); + + // Get the hierarchy string for each category + return categories.map( ( category ) => { + const hierarchy: string[] = [ category.name ]; + let parent = category.parent; + + // Traverse up the category hierarchy until the root category is reached + while ( parent !== 0 ) { + const parentCategory = categoryNamesById[ parent ]; + if ( parentCategory ) { + hierarchy.push( parentCategory.name ); + parent = parentCategory.parent; + } else { + parent = 0; + } + } + + // Reverse the hierarchy array so that the parent category is first + return hierarchy.reverse().join( ' > ' ); + } ); +}; diff --git a/plugins/woo-ai/src/product-description/product-description-button-container.tsx b/plugins/woo-ai/src/product-description/product-description-button-container.tsx index 0931d214379..9e0a0e553a9 100644 --- a/plugins/woo-ai/src/product-description/product-description-button-container.tsx +++ b/plugins/woo-ai/src/product-description/product-description-button-container.tsx @@ -16,6 +16,7 @@ import { import { MAX_TITLE_LENGTH, MIN_TITLE_LENGTH_FOR_DESCRIPTION, + DESCRIPTION_MAX_LENGTH, WOO_AI_PLUGIN_FEATURE_NAME, } from '../constants'; import { InfoModal, StopCompletionBtn, WriteItForMeBtn } from '../components'; @@ -32,8 +33,6 @@ import { Attribute } from '../utils/types'; import { translateApiErrors as getApiError } from '../utils/apiErrors'; import { buildShortDescriptionPrompt } from '../product-short-description/product-short-description-button-container'; -const DESCRIPTION_MAX_LENGTH = 300; - const recordDescriptionTracks = recordTracksFactory( 'description_completion', () => ( { diff --git a/plugins/woo-ai/src/utils/categoryCreator.ts b/plugins/woo-ai/src/utils/categoryCreator.ts new file mode 100644 index 00000000000..d6117fa768b --- /dev/null +++ b/plugins/woo-ai/src/utils/categoryCreator.ts @@ -0,0 +1,186 @@ +/** + * Internal dependencies + */ +import { getAvailableCategories } from '../product-category/utils'; + +interface HTMLWPListElement extends HTMLElement { + wpList: { + settings: { + addAfter: ( + returnedResponse: XMLDocument, + ajaxSettings: object, + wpListSettings: object + ) => void; + }; + }; +} + +declare global { + interface Window { + wpAjax: { + parseAjaxResponse: ( response: object ) => { + responses?: { + data?: string; + }[]; + }; + }; + } +} + +type NewCategory = { + name: string; + parent_id?: number; +}; + +/** + * Creates a category in the product category list. This function can only be used where the product category taxonomy list is available (e.g. on the product edit page). + */ +const createCategory = async ( category: NewCategory ) => { + const newCategoryInput = document.getElementById( + 'newproduct_cat' + ) as HTMLInputElement; + const newCategoryParentSelect = document.getElementById( + 'newproduct_cat_parent' + ) as HTMLSelectElement; + const newCategoryAddButton = document.getElementById( + 'product_cat-add-submit' + ) as HTMLButtonElement; + const addCategoryToggle = document.getElementById( + 'product_cat-add-toggle' + ) as HTMLButtonElement; + const categoryListElement = document.getElementById( + 'product_catchecklist' + ) as HTMLWPListElement; + + if ( + ! [ + newCategoryInput, + newCategoryParentSelect, + newCategoryAddButton, + addCategoryToggle, + categoryListElement, + ].every( Boolean ) + ) { + throw new Error( 'Unable to find the category list elements' ); + } + + // show and hide the category inputs to make sure they are rendered at least once + addCategoryToggle.click(); + addCategoryToggle.click(); + + // Preserve original addAfter function for restoration after use + const orgCatListAddAfter = categoryListElement.wpList.settings.addAfter; + + const categoryCreatedPromise = new Promise< number >( ( resolve ) => { + categoryListElement.wpList.settings.addAfter = ( ...args ) => { + orgCatListAddAfter( ...args ); + categoryListElement.wpList.settings.addAfter = orgCatListAddAfter; + + const parsedResponse = window.wpAjax.parseAjaxResponse( args[ 0 ] ); + if ( ! parsedResponse?.responses?.[ 0 ].data ) { + throw new Error( 'Unable to parse the ajax response' ); + } + + const parsedHtml = new DOMParser().parseFromString( + parsedResponse.responses[ 0 ].data, + 'text/html' + ); + const newlyAddedCategoryCheckbox = Array.from( + parsedHtml.querySelectorAll< HTMLInputElement >( + 'input[name="tax_input[product_cat][]"]' + ) + ).find( ( input ) => { + return ( + input.parentElement?.textContent?.trim() === category.name + ); + } ); + + if ( ! newlyAddedCategoryCheckbox ) { + throw new Error( 'Unable to find the newly added category' ); + } + + resolve( Number( newlyAddedCategoryCheckbox.value ) ); + }; + } ); + + // Fill category name and select parent category if available + newCategoryInput.value = category.name; + if ( category.parent_id ) { + const parentEl = newCategoryParentSelect.querySelector( + 'option[value="' + category.parent_id + '"]' + ) as HTMLOptionElement; + if ( ! parentEl ) { + throw new Error( 'Unable to find the parent category in the list' ); + } + newCategoryParentSelect.value = category.parent_id.toString(); + parentEl.selected = true; + } + + // click the add button to create the category + newCategoryAddButton.click(); + + return categoryCreatedPromise; +}; + +/** + * Gets the list of categories to create from a given path. The path is a string of categories separated by a > character. e.g. "Clothing > Shirts > T-Shirts" + * + * @param categoryPath + */ +const getCategoriesToCreate = async ( + categoryPath: string +): Promise< NewCategory[] > => { + const categories: NewCategory[] = []; + const orderedList = categoryPath.split( ' > ' ); + const availableCategories = await getAvailableCategories(); + let parentCategoryId = 0; + orderedList.every( ( categoryName, index ) => { + const matchingCategory = availableCategories.find( ( category ) => { + return ( + category.name === categoryName && + category.parent === parentCategoryId + ); + } ); + if ( matchingCategory ) { + // This is the parent category ID for the next category in the path + parentCategoryId = matchingCategory.id; + } else { + categories.push( { + name: categoryName, + parent_id: parentCategoryId, + } ); + + for ( let i = index + 1; i < orderedList.length; i++ ) { + categories.push( { + name: orderedList[ i ], + } ); + } + + return false; + } + + return true; + } ); + + return categories; +}; + +/** + * Creates categories from a given path. The path is a string of categories separated by a > character. e.g. "Clothing > Shirts > T-Shirts" + * + * @param categoryPath + */ +export const createCategoriesFromPath = async ( categoryPath: string ) => { + const categoriesToCreate = await getCategoriesToCreate( categoryPath ); + + while ( categoriesToCreate.length ) { + const newCategoryId = await createCategory( + categoriesToCreate.shift() as NewCategory + ); + + if ( categoriesToCreate.length ) { + // Set the parent ID of the next category in the list to the ID of the newly created category so that it is created as a child of the newly created category + categoriesToCreate[ 0 ].parent_id = newCategoryId; + } + } +}; diff --git a/plugins/woo-ai/src/utils/categorySelector.ts b/plugins/woo-ai/src/utils/categorySelector.ts new file mode 100644 index 00000000000..7322497bb1c --- /dev/null +++ b/plugins/woo-ai/src/utils/categorySelector.ts @@ -0,0 +1,72 @@ +/** + * Helper function to select a checkbox if it exists within a element + * + * @param element - The DOM element to check for a checkbox + */ +const checkFirstCheckboxInElement = ( element: HTMLElement ) => { + // Select the checkbox input element if it exists + const checkboxElement: HTMLInputElement | null = element.querySelector( + 'label > input[type="checkbox"]' + ); + + // If the checkbox exists, check it and trigger a 'change' event + if ( checkboxElement ) { + checkboxElement.checked = true; + checkboxElement.dispatchEvent( new Event( 'change' ) ); + } +}; + +/** + * Recursive function to select categories and their children based on the provided ordered list + * + * @param orderedCategories - An ordered list of categories to be selected, starting with the top-level category and ending with the lowest-level category. + * @param categoryElements - A list of HTML List elements representing the categories + */ +const selectCategoriesRecursively = ( + orderedCategories: string[], + categoryElements: HTMLLIElement[] +) => { + const categoryToSelect = orderedCategories[ 0 ]; + + // Find the HTML element that matches the category to be selected + const selectedCategoryElement = categoryElements.find( + ( element ) => + element.querySelector( ':scope > label' )?.textContent?.trim() === + categoryToSelect + ); + + // If the category to be selected doesn't exist, terminate the function + if ( ! selectedCategoryElement ) { + return; + } + + checkFirstCheckboxInElement( selectedCategoryElement ); + + // Select all the child categories, if they exist + const subsequentCategories: string[] = orderedCategories.slice( 1 ); + const childCategoryElements: HTMLLIElement[] = Array.from( + selectedCategoryElement.querySelectorAll( 'ul.children > li' ) + ); + + if ( subsequentCategories.length && childCategoryElements.length ) { + selectCategoriesRecursively( + subsequentCategories, + childCategoryElements + ); + } +}; + +/** + * Main function to select a category and its children from a provided category path + * + * @param categoryPath - The path to the category, each level separated by a > character. + * e.g. "Clothing > Shirts > T-Shirts" + */ +export const selectCategory = ( categoryPath: string ) => { + const categories = categoryPath.split( '>' ).map( ( name ) => name.trim() ); + const categoryListElements: HTMLLIElement[] = Array.from( + document.querySelectorAll( '#product_catchecklist > li' ) + ); + + selectCategoriesRecursively( categories, categoryListElements ); +}; diff --git a/plugins/woo-ai/src/utils/htmlEntities.ts b/plugins/woo-ai/src/utils/htmlEntities.ts new file mode 100644 index 00000000000..a369b412cc0 --- /dev/null +++ b/plugins/woo-ai/src/utils/htmlEntities.ts @@ -0,0 +1,7 @@ +export const decodeHtmlEntities = ( () => { + const textArea = document.createElement( 'textarea' ); + return ( str: string ): string => { + textArea.innerHTML = str; + return textArea.value; + }; +} )(); diff --git a/plugins/woo-ai/src/utils/index.ts b/plugins/woo-ai/src/utils/index.ts index c82c9f23f8b..7c0b0c9727d 100644 --- a/plugins/woo-ai/src/utils/index.ts +++ b/plugins/woo-ai/src/utils/index.ts @@ -3,3 +3,6 @@ export * from './shuffleArray'; export * from './recordTracksFactory'; export * from './get-post-id'; export * from './tiny-tools'; +export * from './productDataInstructionsGenerator'; +export * from './categorySelector'; +export * from './htmlEntities'; diff --git a/plugins/woo-ai/src/utils/productData.ts b/plugins/woo-ai/src/utils/productData.ts index 8aca7573138..b62d3a3d4c4 100644 --- a/plugins/woo-ai/src/utils/productData.ts +++ b/plugins/woo-ai/src/utils/productData.ts @@ -1,21 +1,86 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ import { Attribute } from './types'; import { getTinyContent } from '.'; +export enum ProductProps { + Name = 'name', + Description = 'description', + Categories = 'categories', + Tags = 'tags', + Attributes = 'attributes', +} + +/** + * Retrieves a hierarchy string for the specified category element. This includes the category label and all parent categories separated by a > character. + * + * @param {HTMLInputElement} categoryElement - The category element to get the hierarchy string for. + * @return {string} The hierarchy string for the specified category element. e.g. "Clothing > Shirts > T-Shirts" + */ +const getCategoryHierarchy = ( categoryElement: HTMLElement ) => { + let hierarchy = ''; + let parentElement = categoryElement.parentElement; + + // Traverse up the DOM Tree until a category list item (LI) is found + while ( parentElement ) { + const isListItem = parentElement.tagName.toUpperCase() === 'LI'; + const isRootList = parentElement.id === 'product_catchecklist'; + + if ( isListItem ) { + const categoryLabel = + parentElement.querySelector( 'label' )?.innerText.trim() || ''; + + if ( categoryLabel ) { + hierarchy = hierarchy + ? `${ categoryLabel } > ${ hierarchy }` + : categoryLabel; + } else { + break; + } + } + + if ( isRootList ) { + // If the root category list is found, it means we have reached the top of the hierarchy + break; + } + + parentElement = parentElement.parentElement; + } + + return hierarchy; +}; + +/** + * Function to get selected categories in hierarchical manner. + * + * @return {string[]} Array of category hierarchy strings for each selected category. + */ export const getCategories = (): string[] => { - return Array.from( + // Get all the selected category checkboxes + const checkboxes: NodeListOf< HTMLInputElement > = document.querySelectorAll( - '#taxonomy-product_cat input[name="tax_input[product_cat][]"]' - ) - ) - .filter( - ( item ) => - window.getComputedStyle( item, ':before' ).content !== 'none' - ) - .map( ( item ) => item.nextSibling?.nodeValue?.trim() || '' ) - .filter( Boolean ); + '#taxonomy-product_cat input[type="checkbox"][name="tax_input[product_cat][]"]' + ); + const categoryElements = Array.from( checkboxes ); + + // Filter out the Uncategorized category and return the remaining selected categories + const selectedCategories = categoryElements.filter( ( element ) => { + const categoryLabel = element.parentElement?.innerText.trim(); + + return ( + element.checked && + categoryLabel !== __( 'Uncategorized', 'woocommerce' ) + ); + } ); + + // Get the hierarchy string for each selected category and filter out any empty strings + return selectedCategories.map( getCategoryHierarchy ).filter( Boolean ); }; const isElementVisible = ( element: HTMLElement ) => @@ -88,7 +153,7 @@ export const getDescription = (): string => { const content = document.querySelector( '#content' ) as HTMLInputElement; - const tinyContent = getTinyContent(); + const tinyContent = getTinyContent( 'content', { format: 'text' } ); if ( content && isElementVisible( content ) ) { return content.value; } else if ( tinyContent ) { diff --git a/plugins/woo-ai/src/utils/productDataInstructionsGenerator.ts b/plugins/woo-ai/src/utils/productDataInstructionsGenerator.ts new file mode 100644 index 00000000000..21e4eb8d5d5 --- /dev/null +++ b/plugins/woo-ai/src/utils/productDataInstructionsGenerator.ts @@ -0,0 +1,114 @@ +/** + * Internal dependencies + */ +import { Attribute } from './types'; +import { + getAttributes, + getCategories, + getDescription, + getProductName, + getTags, + ProductProps, +} from '.'; +import { DESCRIPTION_MAX_LENGTH, MAX_TITLE_LENGTH } from '../constants'; + +type PropsFilter = { + excludeProps?: ProductProps[]; + allowedProps?: ProductProps[]; +}; + +type InstructionSet = { + includedProps: string[]; + instructions: string[]; +}; + +/** + * Function to generate prompt instructions for product data. + * + * @param {PropsFilter} propsFilter Object containing the props to be included or excluded from the instructions. + * @param {ProductProps[]} propsFilter.excludeProps Array of props to be excluded from the instructions. + * @param {ProductProps[]} propsFilter.allowedProps Array of props to be included in the instructions. + * + * @return {string[]} Array of prompt instructions. + */ +export const generateProductDataInstructions = ( { + excludeProps, + allowedProps, +}: PropsFilter = {} ): InstructionSet => { + const isPropertyAllowed = ( prop: ProductProps ): boolean => { + if ( allowedProps ) { + return allowedProps.includes( prop ); + } + + if ( excludeProps ) { + return ! excludeProps.includes( prop ); + } + + return true; + }; + + const productName: string = isPropertyAllowed( ProductProps.Name ) + ? getProductName() + : ''; + const productDescription: string = isPropertyAllowed( + ProductProps.Description + ) + ? getDescription() + : ''; + const productCategories: string[] = isPropertyAllowed( + ProductProps.Categories + ) + ? getCategories() + : []; + const productTags: string[] = isPropertyAllowed( ProductProps.Tags ) + ? getTags() + : []; + const productAttributes: Attribute[] = isPropertyAllowed( + ProductProps.Attributes + ) + ? getAttributes() + : []; + + const includedProps: string[] = []; + const productPropsInstructions: string[] = []; + if ( productName ) { + productPropsInstructions.push( + `Name: ${ productName.slice( 0, MAX_TITLE_LENGTH ) }.` + ); + includedProps.push( 'name' ); + } + if ( productDescription ) { + productPropsInstructions.push( + `Description: ${ productDescription.slice( + 0, + DESCRIPTION_MAX_LENGTH + ) }.` + ); + includedProps.push( ProductProps.Description ); + } + if ( productCategories.length ) { + productPropsInstructions.push( + `Product categories: ${ productCategories.join( ', ' ) }.` + ); + includedProps.push( ProductProps.Categories ); + } + if ( productTags.length ) { + productPropsInstructions.push( + `Tagged with: ${ productTags.join( ', ' ) }.` + ); + includedProps.push( ProductProps.Tags ); + } + if ( productAttributes.length ) { + productAttributes.forEach( ( { name, values } ) => { + productPropsInstructions.push( + `${ name }: ${ values.join( ', ' ) }.` + ); + includedProps.push( name ); + } ); + } + + return { + includedProps, + instructions: productPropsInstructions, + }; +}; diff --git a/plugins/woo-ai/src/utils/tiny-tools.ts b/plugins/woo-ai/src/utils/tiny-tools.ts index 86d801c42a0..a31b96f6c0a 100644 --- a/plugins/woo-ai/src/utils/tiny-tools.ts +++ b/plugins/woo-ai/src/utils/tiny-tools.ts @@ -1,5 +1,5 @@ export type TinyContent = { - getContent: () => string; + getContent: ( args?: object ) => string; setContent: ( str: string ) => void; id: string; on: ( event: string, callback: ( event: Event ) => void ) => void; @@ -39,6 +39,6 @@ export const setTinyContent = ( str: string, editorId?: string ) => { } }; -export const getTinyContent = ( editorId?: string ) => { - return getTinyContentObject( editorId )?.getContent() ?? ''; +export const getTinyContent = ( editorId?: string, args?: object ) => { + return getTinyContentObject( editorId )?.getContent( args ) ?? ''; }; From b180910a27691b94a31dae232a470a0a7c90602e Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Tue, 12 Sep 2023 18:47:03 +0100 Subject: [PATCH 46/59] Increase the version of Woo Blocks in "composer.json" --- plugins/woocommerce/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json index d3d208500b3..db68e2048e0 100644 --- a/plugins/woocommerce/composer.json +++ b/plugins/woocommerce/composer.json @@ -23,7 +23,7 @@ "maxmind-db/reader": "^1.11", "pelago/emogrifier": "^6.0", "woocommerce/action-scheduler": "3.6.2", - "woocommerce/woocommerce-blocks": "11.0.0" + "woocommerce/woocommerce-blocks": "11.1.0" }, "require-dev": { "automattic/jetpack-changelogger": "^3.3.0", From efaa9802283a30811ea4e2b2b2d6a46539a786ed Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Tue, 12 Sep 2023 18:51:23 +0100 Subject: [PATCH 47/59] Run "composer update woocommerce/woocommerce-blocks" --- .../bin/composer/mozart/composer.lock | 2 +- .../bin/composer/phpcs/composer.lock | 2 +- .../bin/composer/phpunit/composer.lock | 2 +- .../woocommerce/bin/composer/wp/composer.lock | 2 +- plugins/woocommerce/composer.lock | 18 +++++++++--------- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/woocommerce/bin/composer/mozart/composer.lock b/plugins/woocommerce/bin/composer/mozart/composer.lock index ff99798cb87..db3b69f9755 100644 --- a/plugins/woocommerce/bin/composer/mozart/composer.lock +++ b/plugins/woocommerce/bin/composer/mozart/composer.lock @@ -1169,5 +1169,5 @@ "platform-overrides": { "php": "7.3" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/plugins/woocommerce/bin/composer/phpcs/composer.lock b/plugins/woocommerce/bin/composer/phpcs/composer.lock index b8062e94641..553e6a5cb9f 100644 --- a/plugins/woocommerce/bin/composer/phpcs/composer.lock +++ b/plugins/woocommerce/bin/composer/phpcs/composer.lock @@ -473,5 +473,5 @@ "platform-overrides": { "php": "7.2" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/plugins/woocommerce/bin/composer/phpunit/composer.lock b/plugins/woocommerce/bin/composer/phpunit/composer.lock index 4c3c7a7a6f2..621a95b20f5 100644 --- a/plugins/woocommerce/bin/composer/phpunit/composer.lock +++ b/plugins/woocommerce/bin/composer/phpunit/composer.lock @@ -1711,5 +1711,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/plugins/woocommerce/bin/composer/wp/composer.lock b/plugins/woocommerce/bin/composer/wp/composer.lock index 198bd1d23d5..fec15e9534b 100644 --- a/plugins/woocommerce/bin/composer/wp/composer.lock +++ b/plugins/woocommerce/bin/composer/wp/composer.lock @@ -634,5 +634,5 @@ "platform-overrides": { "php": "7.0" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock index 3450d903fc6..11034bf24d1 100644 --- a/plugins/woocommerce/composer.lock +++ b/plugins/woocommerce/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0c27792dd3b1927fff20aa226d428068", + "content-hash": "6f2cf671cf43bf97d8dcb86bbdd0916a", "packages": [ { "name": "automattic/jetpack-a8c-mc-stats", @@ -1004,16 +1004,16 @@ }, { "name": "woocommerce/woocommerce-blocks", - "version": "11.0.0", + "version": "11.1.0", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-blocks.git", - "reference": "926f174399738ea25de191b1db05cf42ef4635d3" + "reference": "9a9749f72c16bbb9cd75e23061f6b19dec01c262" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/926f174399738ea25de191b1db05cf42ef4635d3", - "reference": "926f174399738ea25de191b1db05cf42ef4635d3", + "url": "https://api.github.com/repos/woocommerce/woocommerce-blocks/zipball/9a9749f72c16bbb9cd75e23061f6b19dec01c262", + "reference": "9a9749f72c16bbb9cd75e23061f6b19dec01c262", "shasum": "" }, "require": { @@ -1023,7 +1023,7 @@ "ext-json": "*" }, "require-dev": { - "mockery/mockery": "1.6.5", + "mockery/mockery": "1.6.6", "nikic/php-parser": "4.16.0 as 1.0.0", "phpdocumentor/reflection": "3.0.1", "phpunit/php-code-coverage": "9.2.27", @@ -1062,9 +1062,9 @@ ], "support": { "issues": "https://github.com/woocommerce/woocommerce-blocks/issues", - "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v11.0.0" + "source": "https://github.com/woocommerce/woocommerce-blocks/tree/v11.1.0" }, - "time": "2023-08-30T10:20:08+00:00" + "time": "2023-09-12T13:56:36+00:00" } ], "packages-dev": [ @@ -3947,5 +3947,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } From abd90ab83b84a76e36b7410dbc864fa8effb9ee8 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Wed, 13 Sep 2023 03:23:24 +0700 Subject: [PATCH 48/59] Update stable tag to 8.1.0 (#40137) woorelease: Update stable tag to 8.1.0 --- plugins/woocommerce/readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/readme.txt b/plugins/woocommerce/readme.txt index 40e58da7f78..40899bb7e26 100644 --- a/plugins/woocommerce/readme.txt +++ b/plugins/woocommerce/readme.txt @@ -4,7 +4,7 @@ Tags: online store, ecommerce, shop, shopping cart, sell online, storefront, che Requires at least: 6.2 Tested up to: 6.3 Requires PHP: 7.4 -Stable tag: 8.0.2 +Stable tag: 8.1.0 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html From f05e8efc17683b56d4941adb702baa16fe8e8cf5 Mon Sep 17 00:00:00 2001 From: Moon Date: Tue, 12 Sep 2023 13:29:06 -0700 Subject: [PATCH 49/59] Track core profiler plugin installation in PHP (#39921) * Use HTTP request to track in cron jobs (action scheduler jobs) * Pass complete data to logger * Track coreprofiler_store_extensions_installed_and_activated when plugin installation is complete * Add changelog * Update plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsyncPluginsInstallLogger.php Co-authored-by: Ilyas Foo * Include start_time in completion data * Update plugins/woocommerce/includes/tracks/class-wc-tracks-event.php Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> * Fix variable naming convention --------- Co-authored-by: Ilyas Foo Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- ...ate-39664-track-plugin-installation-in-php | 4 + .../includes/tracks/class-wc-tracks-event.php | 2 +- .../woocommerce/src/Admin/PluginsHelper.php | 15 ++-- ...gger.php => AsyncPluginsInstallLogger.php} | 87 ++++++++++++++++++- .../PluginsInstallLogger.php | 3 +- 5 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce/changelog/update-39664-track-plugin-installation-in-php rename plugins/woocommerce/src/Admin/PluginsInstallLoggers/{AsynPluginsInstallLogger.php => AsyncPluginsInstallLogger.php} (61%) diff --git a/plugins/woocommerce/changelog/update-39664-track-plugin-installation-in-php b/plugins/woocommerce/changelog/update-39664-track-plugin-installation-in-php new file mode 100644 index 00000000000..3f7729f0e85 --- /dev/null +++ b/plugins/woocommerce/changelog/update-39664-track-plugin-installation-in-php @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Track coreprofiler_store_extensions_installed_and_activated when async installation is complete diff --git a/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php b/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php index 9252db16610..4d2bbe1688f 100644 --- a/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php +++ b/plugins/woocommerce/includes/tracks/class-wc-tracks-event.php @@ -55,7 +55,7 @@ class WC_Tracks_Event { * @return bool Always returns true. */ public function record() { - if ( wp_doing_ajax() || Constants::is_true( 'REST_REQUEST' ) ) { + if ( wp_doing_ajax() || Constants::is_true( 'REST_REQUEST' ) || Constants::is_true( 'WP_CLI' ) || wp_doing_cron() ) { return WC_Tracks_Client::record_event( $this ); } diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index e5e84080524..3deb9589856 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -186,11 +186,12 @@ class PluginsHelper { include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php'; - $existing_plugins = self::get_installed_plugins_paths(); - $installed_plugins = array(); - $results = array(); - $time = array(); - $errors = new WP_Error(); + $existing_plugins = self::get_installed_plugins_paths(); + $installed_plugins = array(); + $results = array(); + $time = array(); + $errors = new WP_Error(); + $install_start_time = time(); foreach ( $plugins as $plugin ) { $slug = sanitize_key( $plugin ); @@ -305,8 +306,6 @@ class PluginsHelper { $logger && $logger->installed( $plugin, $time[ $plugin ] ); } - $logger && $logger->complete(); - $data = array( 'installed' => $installed_plugins, 'results' => $results, @@ -314,6 +313,8 @@ class PluginsHelper { 'time' => $time, ); + $logger && $logger->complete( array_merge( $data, array( 'start_time' => $install_start_time ) ) ); + return $data; } diff --git a/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsynPluginsInstallLogger.php b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsyncPluginsInstallLogger.php similarity index 61% rename from plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsynPluginsInstallLogger.php rename to plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsyncPluginsInstallLogger.php index c1f6d234f48..926a73cf58a 100644 --- a/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsynPluginsInstallLogger.php +++ b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsyncPluginsInstallLogger.php @@ -20,7 +20,7 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger { * @param string $option_name option name. */ public function __construct( string $option_name ) { - $this->option_name = $option_name; + $this->option_name = $option_name; add_option( $this->option_name, array( @@ -135,14 +135,97 @@ class AsyncPluginsInstallLogger implements PluginsInstallLogger { /** * Record completed_time. * + * @param array $data return data from install_plugins(). * @return void */ - public function complete() { + public function complete( $data = array() ) { $option = $this->get(); $option['complete_time'] = time(); $option['status'] = 'complete'; + $this->track( $data ); $this->update( $option ); } + + private function get_plugin_track_key( $id ) { + $slug = explode( ':', $id )[0]; + $key = preg_match( '/^woocommerce(-|_)payments$/', $slug ) + ? 'wcpay' + : explode( ':', str_replace( '-', '_', $slug ) )[0]; + return $key; + } + + /** + * Returns time frame for a given time in milliseconds. + * + * @param int $timeInMs - time in milliseconds + * + * @return string - Time frame. + */ + function get_timeframe( $timeInMs ) { + $time_frames = [ + [ + 'name' => '0-2s', + 'max' => 2, + ], + [ + 'name' => '2-5s', + 'max' => 5, + ], + [ + 'name' => '5-10s', + 'max' => 10, + ], + [ + 'name' => '10-15s', + 'max' => 15, + ], + [ + 'name' => '15-20s', + 'max' => 20, + ], + [ + 'name' => '20-30s', + 'max' => 30, + ], + [ + 'name' => '30-60s', + 'max' => 60, + ], + [ 'name' => '>60s' ], + ]; + + foreach ( $time_frames as $time_frame ) { + if ( ! isset( $time_frame['max'] ) ) { + return $time_frame['name']; + } + if ( $timeInMs < $time_frame['max'] * 1000 ) { + return $time_frame['name']; + } + } + } + + private function track( $data ) { + $track_data = array( + 'success' => true, + 'installed_extensions' => array_map( + function( $extension ) { + return $this->get_plugin_track_key( $extension ); + }, + $data['installed'] + ), + 'total_time' => $this->get_timeframe( ( time() - $data['start_time'] ) * 1000 ), + ); + + foreach ( $data['installed'] as $plugin ) { + if ( ! isset( $data['time'][ $plugin ] ) ) { + continue; + } + + $track_data[ 'install_time_' . $this->get_plugin_track_key( $plugin ) ] = $this->get_timeframe( $data['time'][ $plugin ] ); + } + + wc_admin_record_tracks_event( 'coreprofiler_store_extensions_installed_and_activated', $track_data ); + } } diff --git a/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php index d5d89342306..7cb708e09b3 100644 --- a/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php +++ b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php @@ -44,8 +44,9 @@ interface PluginsInstallLogger { /** * Called when all plugins are processed. * + * @param array $data return data from install_plugins(). * @return mixed */ - public function complete(); + public function complete( $data = array() ); } From 4466fe4418f05caa0c2f7c1852cc61005625f8ff Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Tue, 12 Sep 2023 23:59:11 +0100 Subject: [PATCH 50/59] Add changelog --- .../woocommerce/changelog/update-woocommerce-blocks-11.1.0 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.0 diff --git a/plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.0 b/plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.0 new file mode 100644 index 00000000000..bce6d0ca513 --- /dev/null +++ b/plugins/woocommerce/changelog/update-woocommerce-blocks-11.1.0 @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Update WooCommerce Blocks to 11.1.0 From 007cf95d9392eba1a980851bad0033943798881b Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 13 Sep 2023 13:14:50 +0530 Subject: [PATCH 51/59] Remove unused code + reduce number of delete ops. --- .../abstract-wc-order-data-store-cpt.php | 15 ++++++++------- .../Orders/OrdersTableDataStoreTests.php | 11 +++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php index a28d3624da4..4965eef8d58 100644 --- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -724,18 +724,15 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme } if ( is_array( $existing_meta_data[ $meta_data->key ] ) ) { - $key_search = array_search( $meta_data->value, $existing_meta_data[ $meta_data->key ], true ); - if ( false !== $key_search ) { - unset( $existing_meta_data[ $meta_data->key ][ $key_search ] ); + $value_index = array_search( $meta_data->value, $existing_meta_data[ $meta_data->key ], true ); + if ( false !== $value_index ) { + unset( $existing_meta_data[ $meta_data->key ][ $value_index ] ); if ( 0 === count( $existing_meta_data[ $meta_data->key ] ) ) { unset( $existing_meta_data[ $meta_data->key ] ); } continue; } } - - unset( $existing_meta_data[ $meta_data->key ] ); - delete_post_meta( $order->get_id(), $meta_data->key, $meta_data->value ); } add_post_meta( $order->get_id(), $meta_data->key, $meta_data->value, false ); } @@ -749,7 +746,11 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme ); foreach ( $keys_to_delete as $meta_key ) { - delete_post_meta( $order->get_id(), $meta_key ); + if ( isset( $existing_meta_data[ $meta_key ] ) ) { + foreach ( $existing_meta_data[ $meta_key ] as $meta_value ) { + delete_post_meta( $order->get_id(), $meta_key, $meta_value ); + } + } } $this->update_post_meta( $order ); diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php index 31084a67d34..3998d134c5d 100644 --- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php +++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php @@ -2962,5 +2962,16 @@ class OrdersTableDataStoreTests extends HposTestCase { */ $post_meta = get_post_meta( $order->get_id(), 'test_key' ); $this->assertTrue( in_array( 'test_value3', $post_meta, true ) ); + + $order->set_date_modified( gmdate( 'Y-m-d H:i:s', strtotime( '-2 day' ) ) ); + $order->save(); + + add_post_meta( $order->get_id(), 'test_key2', 'test_value5' ); + $order->add_meta_data( 'test_key2', 'test_value4' ); + $order->save(); + + $post_meta = get_post_meta( $order->get_id(), 'test_key2' ); + $this->assertTrue( in_array( 'test_value4', $post_meta, true ) ); + $this->assertFalse( in_array( 'test_value5', $post_meta, true ) ); } } From 4495a6a49e5351497cf8769a2708cdab5fcd1cc9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 13 Sep 2023 16:01:28 +0800 Subject: [PATCH 52/59] Add customize store - fonts (#40082) * Add customize store fonts * Add changefile(s) from automation for the following project(s): woocommerce * Add inline comment for font-hosting note * Update plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx Co-authored-by: Ilyas Foo * Remove duplicated External dependencies --------- Co-authored-by: github-actions Co-authored-by: Ilyas Foo --- .../assembler-hub/auto-block-preview.tsx | 25 + .../font-pairing-variations/constants.ts | 503 ++++++++++++++++++ .../font-families-loader.tsx | 51 ++ .../font-pairing-variations/index.tsx | 30 ++ .../font-pairing-variations/preview.tsx | 176 ++++++ .../sidebar/global-styles/index.ts | 1 + .../global-styles/variation-container.jsx | 23 +- .../sidebar-navigation-screen-typography.tsx | 31 +- .../customize-store/assembler-hub/style.scss | 51 +- plugins/woocommerce/changelog/add-cys-fonts | 4 + 10 files changed, 871 insertions(+), 24 deletions(-) create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/font-families-loader.tsx create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx create mode 100644 plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/preview.tsx create mode 100644 plugins/woocommerce/changelog/add-cys-fonts diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx index c4d8ec1e9b5..fa9f22736fb 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/auto-block-preview.tsx @@ -11,14 +11,23 @@ import { Disabled } from '@wordpress/components'; import { __unstableEditorStyles as EditorStyles, __unstableIframe as Iframe, + privateApis as blockEditorPrivateApis, BlockList, // @ts-ignore No types for this exist yet. } from '@wordpress/block-editor'; +// @ts-ignore No types for this exist yet. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; +import { noop } from 'lodash'; /** * Internal dependencies */ import { LogoBlockContext } from './logo-block-context'; +import { + FontFamiliesLoader, + FontFamily, +} from './sidebar/global-styles/font-pairing-variations/font-families-loader'; +import { SYSTEM_FONT_SLUG } from './sidebar/global-styles/font-pairing-variations/constants'; const MAX_HEIGHT = 2000; // @ts-ignore No types for this exist yet. @@ -27,6 +36,8 @@ const { Provider: DisabledProvider } = Disabled.Context; // This is used to avoid rendering the block list if the sizes change. let MemoizedBlockList: typeof BlockList | undefined; +const { useGlobalSetting } = unlock( blockEditorPrivateApis ); + export type ScaledBlockPreviewProps = { viewportWidth?: number; containerWidth: number; @@ -48,6 +59,13 @@ function ScaledBlockPreview( { onClickNavigationItem, }: ScaledBlockPreviewProps ) { const { setLogoBlock } = useContext( LogoBlockContext ); + const [ fontFamilies ] = useGlobalSetting( + 'typography.fontFamilies.theme' + ) as [ FontFamily[] ]; + + const externalFontFamilies = fontFamilies.filter( + ( { slug } ) => slug !== SYSTEM_FONT_SLUG + ); if ( ! viewportWidth ) { viewportWidth = containerWidth; @@ -254,6 +272,13 @@ function ScaledBlockPreview( { { contentResizeListener } + { /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ } + { externalFontFamilies.length === 2 && ( + + ) } ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts new file mode 100644 index 00000000000..e018432f046 --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts @@ -0,0 +1,503 @@ +export const FONT_PREVIEW_LARGE_WIDTH = 136; +export const FONT_PREVIEW_LARGE_HEIGHT = 106; +export const FONT_PREVIEW_WIDTH = 120; +export const FONT_PREVIEW_HEIGHT = 74; +export const SYSTEM_FONT_SLUG = 'system-font'; + +// Generated from /wpcom/v2/sites/{site_id}/global-styles-variation/font-pairings +// TODO: Consider creating an API endpoint for this data +export const FONT_PAIRINGS = [ + { + title: 'Bodoni Moda + Overpass', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Bodoni Moda', + slug: 'bodoni-moda', + }, + { + fontFamily: 'Overpass', + slug: 'overpass', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: 'var(--wp--preset--font-family--overpass)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--bodoni-moda)', + fontStyle: 'normal', + fontWeight: '400', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--bodoni-moda)', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--bodoni-moda)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--overpass)', + fontSize: 'var(--wp--preset--font-size--medium)', + fontStyle: 'normal', + fontWeight: '300', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Commissioner + Crimson Pro', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Commissioner', + slug: 'commissioner', + }, + { + fontFamily: 'Crimson Pro', + slug: 'crimson-pro', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--commissioner)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--commissioner)', + fontStyle: 'normal', + fontWeight: '300', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--commissioner)', + fontWeight: '300', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--commissioner)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--crimson-pro)', + fontSize: 'var(--wp--preset--font-size--medium)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Libre Baskerville + DM Sans', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Libre Baskerville', + slug: 'libre-baskerville', + }, + { + fontFamily: 'DM Sans', + slug: 'dm-sans', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: 'var(--wp--preset--font-family--dm-sans)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-baskerville)', + fontStyle: 'normal', + fontWeight: '700', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-baskerville)', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-baskerville)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--dm-sans)', + fontSize: 'var(--wp--preset--font-size--small)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Libre Franklin + EB Garamond', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Libre Franklin', + slug: 'libre-franklin', + }, + { + fontFamily: 'EB Garamond', + slug: 'eb-garamond', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-franklin)', + fontSize: 'var(--wp--preset--font-size--small)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-franklin)', + fontStyle: 'normal', + fontWeight: '700', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-franklin)', + fontWeight: '500', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--libre-franklin)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--eb-garamond)', + fontSize: 'var(--wp--preset--font-size--medium)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Montserrat + Arvo', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Montserrat', + slug: 'montserrat', + }, + { + fontFamily: 'Arvo', + slug: 'arvo', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--montserrat)', + fontStyle: 'normal', + fontWeight: '500', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--montserrat)', + fontStyle: 'normal', + fontWeight: '700', + lineHeight: '1.4', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--montserrat)', + fontWeight: '700', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--montserrat)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--arvo)', + fontSize: 'var(--wp--preset--font-size--small)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Playfair Display + Fira Sans', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Playfair Display', + slug: 'playfair-display', + }, + { + fontFamily: 'Fira Sans', + slug: 'fira-sans', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: 'var(--wp--preset--font-family--fira-sans)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--playfair-display)', + fontStyle: 'italic', + fontWeight: '400', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--playfair-display)', + fontStyle: 'italic', + fontWeight: '400', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--playfair-display)', + fontStyle: 'italic', + fontWeight: '400', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--fira-sans)', + fontSize: 'var(--wp--preset--font-size--medium)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Rubik + Inter', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Rubik', + slug: 'rubik', + }, + { + fontFamily: 'Inter', + slug: 'inter', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: 'var(--wp--preset--font-family--inter)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: 'var(--wp--preset--font-family--rubik)', + fontStyle: 'normal', + fontWeight: '800', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: 'var(--wp--preset--font-family--rubik)', + fontWeight: '800', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: 'var(--wp--preset--font-family--rubik)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--inter)', + fontSize: 'var(--wp--preset--font-size--medium)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, + { + title: 'Space Mono + Roboto', + version: 2, + settings: { + typography: { + fontFamilies: { + theme: [ + { + fontFamily: 'Space Mono', + slug: 'space-mono', + }, + { + fontFamily: 'Roboto', + slug: 'roboto', + }, + ], + }, + }, + }, + styles: { + elements: { + button: { + typography: { + fontFamily: 'var(--wp--preset--font-family--roboto)', + fontWeight: '400', + lineHeight: '1', + }, + }, + heading: { + typography: { + fontFamily: + 'var(--wp--preset--font-family--space-mono)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.15', + }, + }, + }, + blocks: { + 'core/site-title': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--space-mono)', + fontStyle: 'normal', + fontWeight: '400', + }, + }, + 'core/post-navigation-link': { + typography: { + fontFamily: + 'var(--wp--preset--font-family--space-mono)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--roboto)', + fontSize: 'var(--wp--preset--font-size--small)', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '1.6', + }, + }, + }, +]; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/font-families-loader.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/font-families-loader.tsx new file mode 100644 index 00000000000..d11d5f3229a --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/font-families-loader.tsx @@ -0,0 +1,51 @@ +// TODO: We should Download webfonts and host them locally on a site before launching CYS in Core. +// Load font families from wp.com. + +/** + * External dependencies + */ +import { useMemo } from '@wordpress/element'; + +export type FontFamily = { + fontFamily: string; + name: string; + slug: string; +}; + +type Props = { + fontFamilies: FontFamily[]; + onLoad?: () => void; +}; + +// See https://developers.google.com/fonts/docs/css2 +const FONT_API_BASE = 'https://fonts-api.wp.com/css2'; + +const FONT_AXIS = 'ital,wght@0,400;0,700;1,400;1,700'; + +export const FontFamiliesLoader = ( { fontFamilies, onLoad }: Props ) => { + const params = useMemo( + () => + new URLSearchParams( [ + ...fontFamilies.map( ( { fontFamily } ) => [ + 'family', + `${ fontFamily }:${ FONT_AXIS }`, + ] ), + [ 'display', 'swap' ], + ] ), + fontFamilies + ); + + if ( ! params.getAll( 'family' ).length ) { + return null; + } + + return ( + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx new file mode 100644 index 00000000000..04cbc65ac0c --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/index.tsx @@ -0,0 +1,30 @@ +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +// @ts-ignore No types for this exist yet. +import { __experimentalGrid as Grid } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { FONT_PAIRINGS } from './constants'; +import { VariationContainer } from '../variation-container'; +import { FontPairingVariationPreview } from './preview'; + +export const FontPairing = () => { + return ( + + { FONT_PAIRINGS.map( ( variation, index ) => ( + + + + ) ) } + + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/preview.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/preview.tsx new file mode 100644 index 00000000000..cfce63a730b --- /dev/null +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/preview.tsx @@ -0,0 +1,176 @@ +// Reference: https://github.com/Automattic/wp-calypso/blob/d3c9b16fb99ce242f61baa21119b7c20f8823be6/packages/global-styles/src/components/font-pairing-variations/preview.tsx +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * External dependencies + */ +import { + // @ts-ignore No types for this exist yet. + __experimentalHStack as HStack, + // @ts-ignore No types for this exist yet. + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { useResizeObserver, useViewportMatch } from '@wordpress/compose'; +import { useMemo, useState } from '@wordpress/element'; +import { + privateApis as blockEditorPrivateApis, + // @ts-ignore no types exist yet. +} from '@wordpress/block-editor'; +// @ts-ignore No types for this exist yet. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; +import { GlobalStylesVariationIframe } from '../global-styles-variation-iframe'; +import { FontFamiliesLoader, FontFamily } from './font-families-loader'; +import { + FONT_PREVIEW_LARGE_WIDTH, + FONT_PREVIEW_LARGE_HEIGHT, + FONT_PREVIEW_WIDTH, + FONT_PREVIEW_HEIGHT, + SYSTEM_FONT_SLUG, +} from './constants'; + +const { useGlobalStyle, useGlobalSetting } = unlock( blockEditorPrivateApis ); + +const DEFAULT_LARGE_FONT_STYLES: React.CSSProperties = { + fontSize: '13vw', // 18px for min-width wide breakpoint and 15px for max-width wide + lineHeight: '20px', + color: '#000000', +}; + +export const FontPairingVariationPreview = () => { + const [ fontFamilies ] = useGlobalSetting( + 'typography.fontFamilies.theme' + ) as [ FontFamily[] ]; + + const [ textFontFamily = 'serif' ] = useGlobalStyle( + 'typography.fontFamily' + ); + const [ textFontStyle = 'normal' ] = useGlobalStyle( + 'typography.fontStyle' + ); + const [ textLetterSpacing = '-0.15px' ] = useGlobalStyle( + 'typography.letterSpacing' + ); + const [ textFontWeight = 400 ] = useGlobalStyle( 'typography.fontWeight' ); + + const [ headingFontFamily = textFontFamily ] = useGlobalStyle( + 'elements.heading.typography.fontFamily' + ); + const [ headingFontStyle = textFontStyle ] = useGlobalStyle( + 'elements.heading.typography.fontStyle' + ); + const [ headingFontWeight = textFontWeight ] = useGlobalStyle( + 'elements.heading.typography.fontWeight' + ); + const [ headingLetterSpacing = textLetterSpacing ] = useGlobalStyle( + 'elements.heading.typography.letterSpacing' + ); + + const [ containerResizeListener, { width } ] = useResizeObserver(); + const isDesktop = useViewportMatch( 'large' ); + const defaultWidth = isDesktop + ? FONT_PREVIEW_LARGE_WIDTH + : FONT_PREVIEW_WIDTH; + const defaultHeight = isDesktop + ? FONT_PREVIEW_LARGE_HEIGHT + : FONT_PREVIEW_HEIGHT; + const ratio = width ? width / defaultWidth : 1; + const normalizedHeight = Math.ceil( defaultHeight * ratio ); + const externalFontFamilies = fontFamilies.filter( + ( { slug } ) => slug !== SYSTEM_FONT_SLUG + ); + const [ isLoaded, setIsLoaded ] = useState( ! externalFontFamilies.length ); + const getFontFamilyName = ( targetFontFamily: string ) => { + const fontFamily = fontFamilies.find( + ( { fontFamily: _fontFamily } ) => _fontFamily === targetFontFamily + ); + return fontFamily?.name || fontFamily?.fontFamily || targetFontFamily; + }; + + const textFontFamilyName = useMemo( + () => getFontFamilyName( textFontFamily ), + [ textFontFamily, fontFamilies ] + ); + + const headingFontFamilyName = useMemo( + () => getFontFamilyName( headingFontFamily ), + [ headingFontFamily, fontFamilies ] + ); + + const handleOnLoad = () => setIsLoaded( true ); + + return ( + + <> +
    +
    + + +
    + { headingFontFamilyName } +
    +
    + { textFontFamilyName } +
    +
    +
    +
    +
    + + +
    + ); +}; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts index 5aee47c9b64..58142737e6f 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/index.ts @@ -1,2 +1,3 @@ export { ColorPalette } from './color-palette-variations'; export { ColorPanel } from './color-panel'; +export { FontPairing } from './font-pairing-variations'; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx index 088bb2740e0..71aabc0679a 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/variation-container.jsx @@ -9,10 +9,9 @@ import { __, sprintf } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider'; import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; +import { isEqual } from 'lodash'; -const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( - blockEditorPrivateApis -); +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); export const VariationContainer = ( { variation, children } ) => { const { base, user, setUserConfig } = useContext( GlobalStylesContext ); @@ -31,8 +30,14 @@ export const VariationContainer = ( { variation, children } ) => { const selectVariation = () => { setUserConfig( () => { return { - settings: variation.settings, - styles: variation.styles, + settings: mergeBaseAndUserConfigs( + user.settings, + variation.settings + ), + styles: mergeBaseAndUserConfigs( + user.styles, + variation.styles + ), }; } ); }; @@ -45,7 +50,13 @@ export const VariationContainer = ( { variation, children } ) => { }; const isActive = useMemo( () => { - return areGlobalStyleConfigsEqual( user, variation ); + if ( variation.settings.color ) { + return isEqual( variation.settings.color, user.settings.color ); + } + return isEqual( + variation.settings.typography, + user.settings.typography + ); }, [ user, variation ] ); let label = variation?.title; diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx index 8353edcc424..f0a9c2fd3e3 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/sidebar-navigation-screen-typography.tsx @@ -1,17 +1,36 @@ +/* eslint-disable @woocommerce/dependency-group */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /** * External dependencies */ import { __ } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; import { Link } from '@woocommerce/components'; +import { useSelect } from '@wordpress/data'; +// @ts-ignore no types exist yet. +import { BlockEditorProvider } from '@wordpress/block-editor'; +import { noop } from 'lodash'; +// @ts-ignore No types for this exist yet. +import { store as editSiteStore } from '@wordpress/edit-site/build-module/store'; +// @ts-ignore No types for this exist yet. +import { unlock } from '@wordpress/edit-site/build-module/lock-unlock'; /** * Internal dependencies */ import { SidebarNavigationScreen } from './sidebar-navigation-screen'; import { ADMIN_URL } from '~/utils/admin-settings'; +import { FontPairing } from './global-styles'; export const SidebarNavigationScreenTypography = () => { + const { storedSettings } = useSelect( ( select ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + + return { + storedSettings: getSettings( false ), + }; + }, [] ); + return ( { } ) } content={ - <> -
    - +
    + + + +
    } /> ); diff --git a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss index 05e760eb9f3..12754fad61d 100644 --- a/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss +++ b/plugins/woocommerce-admin/client/customize-store/assembler-hub/style.scss @@ -357,6 +357,42 @@ .woocommerce-customize-store_sidebar-color-content { width: 324px; + + .woocommerce-customize-store_global-styles-variations_item { + border-radius: 2px; + padding: 2.5px; + + .woocommerce-customize-store_global-styles-variations_item-preview { + border: 1px solid #dcdcde; + background: #fff; + } + + &:hover, + &.is-active { + box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color), 0 0 0 2.5px #fff; + } + } + } + + .woocommerce-customize-store_sidebar-typography-content { + width: 324px; + + .woocommerce-customize-store_global-styles-variations_item { + border-radius: 2px; + border: 1px solid #dcdcde; + background: #fff; + + &:hover, + &.is-active { + border-radius: 2px; + border: 1.5px solid var(--wp-admin-theme-color); + background: #fff; + } + } + + .global-styles-variation-container__iframe { + box-shadow: none; + } } /* Preview Canvas */ @@ -473,18 +509,3 @@ margin-left: 12px; } } - -.woocommerce-customize-store_global-styles-variations_item { - border-radius: 2px; - padding: 2.5px; - - .woocommerce-customize-store_global-styles-variations_item-preview { - border: 1px solid #dcdcde; - background: #fff; - } - - &:hover, - &.is-active { - box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color), 0 0 0 2.5px #fff; - } -} diff --git a/plugins/woocommerce/changelog/add-cys-fonts b/plugins/woocommerce/changelog/add-cys-fonts new file mode 100644 index 00000000000..b6e6400d8d0 --- /dev/null +++ b/plugins/woocommerce/changelog/add-cys-fonts @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add customize store - fonts From 9d36bbe10ba614ce8bdee7f4b3fed8da69662bb8 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 13 Sep 2023 14:49:31 +0530 Subject: [PATCH 53/59] Handle serialized values. --- .../includes/data-stores/abstract-wc-order-data-store-cpt.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php index 4965eef8d58..8a5d6060e7c 100644 --- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php +++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -748,7 +748,7 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme foreach ( $keys_to_delete as $meta_key ) { if ( isset( $existing_meta_data[ $meta_key ] ) ) { foreach ( $existing_meta_data[ $meta_key ] as $meta_value ) { - delete_post_meta( $order->get_id(), $meta_key, $meta_value ); + delete_post_meta( $order->get_id(), $meta_key, maybe_unserialize( $meta_value ) ); } } } From d1f935cca4c0edf875162d833e7bd514419ef2a6 Mon Sep 17 00:00:00 2001 From: Kyle Nel <22053773+kdevnel@users.noreply.github.com> Date: Wed, 13 Sep 2023 11:32:05 +0200 Subject: [PATCH 54/59] Add country to in-app marketplace search (#40091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Marketplace: remove orphaned components * Marketplace: add country to search query * Add changefile(s) from automation for the following project(s): woocommerce Co-authored-by: Cem Ünalan --------- Co-authored-by: github-actions Co-authored-by: Cem Ünalan --- .../components/extensions/extensions.tsx | 6 +++ .../header-search-button.tsx | 33 ---------------- .../header-search/header-search.tsx | 39 ------------------- .../fix-wccom-18031-marketplace-search-locale | 5 +++ .../admin/helper/class-wc-helper-admin.php | 1 + 5 files changed, 12 insertions(+), 72 deletions(-) delete mode 100644 plugins/woocommerce-admin/client/marketplace/components/header-search-button/header-search-button.tsx delete mode 100644 plugins/woocommerce-admin/client/marketplace/components/header-search/header-search.tsx create mode 100644 plugins/woocommerce/changelog/fix-wccom-18031-marketplace-search-locale diff --git a/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx b/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx index 7985eff416a..bd6f36cd18a 100644 --- a/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx +++ b/plugins/woocommerce-admin/client/marketplace/components/extensions/extensions.tsx @@ -16,6 +16,7 @@ import ProductLoader from '../product-loader/product-loader'; import NoResults from '../product-list-content/no-results'; import { Product, SearchAPIProductType } from '../product-list/types'; import { MARKETPLACE_SEARCH_API_PATH, MARKETPLACE_HOST } from '../constants'; +import { getAdminSetting } from '../../../utils/admin-settings'; export default function Extensions(): JSX.Element { const [ productList, setProductList ] = useState< Product[] >( [] ); @@ -38,6 +39,11 @@ export default function Extensions(): JSX.Element { params.append( 'category', query.category ); } + const wccomSettings = getAdminSetting( 'wccomHelper', false ); + if ( wccomSettings.storeCountry ) { + params.append( 'country', wccomSettings.storeCountry ); + } + const wccomSearchEndpoint = MARKETPLACE_HOST + MARKETPLACE_SEARCH_API_PATH + diff --git a/plugins/woocommerce-admin/client/marketplace/components/header-search-button/header-search-button.tsx b/plugins/woocommerce-admin/client/marketplace/components/header-search-button/header-search-button.tsx deleted file mode 100644 index 0769799cadc..00000000000 --- a/plugins/woocommerce-admin/client/marketplace/components/header-search-button/header-search-button.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ - -export default function HeaderSearchButton() { - return ( - - ); -} diff --git a/plugins/woocommerce-admin/client/marketplace/components/header-search/header-search.tsx b/plugins/woocommerce-admin/client/marketplace/components/header-search/header-search.tsx deleted file mode 100644 index 200eca948d4..00000000000 --- a/plugins/woocommerce-admin/client/marketplace/components/header-search/header-search.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; - -export default function HeaderSearch() { - return ( -
    - - -
    - ); -} diff --git a/plugins/woocommerce/changelog/fix-wccom-18031-marketplace-search-locale b/plugins/woocommerce/changelog/fix-wccom-18031-marketplace-search-locale new file mode 100644 index 00000000000..73a5eda923e --- /dev/null +++ b/plugins/woocommerce/changelog/fix-wccom-18031-marketplace-search-locale @@ -0,0 +1,5 @@ +Significance: patch +Type: update +Comment: Add country code to Marketplace search API requests. + + diff --git a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php index 0d1484be925..e2ee1348b12 100644 --- a/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php +++ b/plugins/woocommerce/includes/admin/helper/class-wc-helper-admin.php @@ -44,6 +44,7 @@ class WC_Helper_Admin { 'connectURL' => self::get_connection_url(), 'userEmail' => $auth_user_email, 'userAvatar' => get_avatar_url( $auth_user_email, array( 'size' => '48' ) ), + 'storeCountry' => wc_get_base_location()['country'], ); return $settings; From 252c349fd25309ad657aeba3e326d863132a8831 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 13 Sep 2023 16:30:19 +0530 Subject: [PATCH 55/59] Updated docs. --- .../src/Internal/DataStores/Orders/OrdersTableDataStore.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 4745d46d346..dc7d37f2bc0 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -2938,6 +2938,10 @@ CREATE TABLE $meta_table ( /** * Helper function to check whether the modified date needs to be updated after a meta save. * + * This method prevents order->save() call multiple times in the same request after any meta update by checking if: + * 1. Order modified date is already the current date, no updates needed in this case. + * 2. If there are changes already queued for order object, then we don't need to update the modified date as it will be updated ina subsequent save() call. + * * @param WC_Order $order Order object. * * @return bool Whether the modified date needs to be updated. From 250bc9ea4e12fd6ae63375500a87973865ef7ea6 Mon Sep 17 00:00:00 2001 From: jonathansadowski Date: Wed, 13 Sep 2023 21:12:51 +0700 Subject: [PATCH 56/59] Allow changelog in build zip (#40133) * Allow changelog in build zip * Add changefile(s) from automation for the following project(s): woocommerce --------- Co-authored-by: github-actions --- plugins/woocommerce/.distignore | 1 - .../woocommerce/changelog/update-distignore-allow-changelog | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/update-distignore-allow-changelog diff --git a/plugins/woocommerce/.distignore b/plugins/woocommerce/.distignore index c9e2daa5b4e..ba8ad0d212a 100644 --- a/plugins/woocommerce/.distignore +++ b/plugins/woocommerce/.distignore @@ -10,7 +10,6 @@ /tests/ /e2e/ babel.config.js -changelog.txt composer.* contributors.html docker-compose.yaml diff --git a/plugins/woocommerce/changelog/update-distignore-allow-changelog b/plugins/woocommerce/changelog/update-distignore-allow-changelog new file mode 100644 index 00000000000..ff02ab5ca04 --- /dev/null +++ b/plugins/woocommerce/changelog/update-distignore-allow-changelog @@ -0,0 +1,5 @@ +Significance: patch +Type: dev +Comment: This PR updates the .distignore such that the changelog.txt file is included in builds, and does not itself need a changelog entry. + + From fbd1224fee9151e541415ae29e8910684b76920f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 09:14:55 -0500 Subject: [PATCH 57/59] Update changelog.txt from release 8.1.0 (#40138) Prep trunk post release 8.1.0 Co-authored-by: WooCommerce Bot --- changelog.txt | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/changelog.txt b/changelog.txt index 2ab8551f5ad..06194a6a6b2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,95 @@ == Changelog == += 8.1.0 2023-09-12 = + +**WooCommerce** + +* Fix - Update modified date when a metadata is saved for HPOS. [#39911](https://github.com/woocommerce/woocommerce/pull/39911) +* Fix - Fix edgecase performance issues around incentives caching. [#39958](https://github.com/woocommerce/woocommerce/pull/39958) +* Fix - Add migration to move incorrectly stored payment token IDS to HPOS tables from postmeta. [#39724](https://github.com/woocommerce/woocommerce/pull/39724) +* Fix - Address more PHP 8.1+ deprecation warnings in wc-admin code. [#38774](https://github.com/woocommerce/woocommerce/pull/38774) +* Fix - Adds display of postcodes to Vietnam addresses. [#39403](https://github.com/woocommerce/woocommerce/pull/39403) +* Fix - Always return bool values from WPCacheEngine functions when expected. [#39819](https://github.com/woocommerce/woocommerce/pull/39819) +* Fix - Be more precise when checking submission data from the email verification form on the order confirmation screen. [#39479](https://github.com/woocommerce/woocommerce/pull/39479) +* Fix - Bring HPOS order hooks in line with the posts implementation. [#39694](https://github.com/woocommerce/woocommerce/pull/39694) +* Fix - Connect WC_Install's create_tables to HPOS tables when its active. [#39682](https://github.com/woocommerce/woocommerce/pull/39682) +* Fix - Disable read on sync while backfilling. [#39450](https://github.com/woocommerce/woocommerce/pull/39450) +* Fix - Ensure refund meta data is saved correctly when HPOS is enabled. [#39700](https://github.com/woocommerce/woocommerce/pull/39700) +* Fix - Ensure that the full discount is ignored in free shipping minimum order calculations when ignore_discount setting is enabled [#39155](https://github.com/woocommerce/woocommerce/pull/39155) +* Fix - Fixed a race condition that was causing page views on intro-opt-in page to be sent before tracks was enabled. [#39508](https://github.com/woocommerce/woocommerce/pull/39508) +* Fix - Fixes WooCommerce knowledge base API returning empty posts. [#39809](https://github.com/woocommerce/woocommerce/pull/39809) +* Fix - Fix failure due to multiple h2 tags in the Product Vendors plugin [#38717](https://github.com/woocommerce/woocommerce/pull/38717) +* Fix - Fix Storefront recommendation link and missing image in Marketplace [#39294](https://github.com/woocommerce/woocommerce/pull/39294) +* Fix - include post_ID in HPOS order edit screen [#39321](https://github.com/woocommerce/woocommerce/pull/39321) +* Fix - Limit index length to 191 characters by default, additionally connect HPOS to verify DB tooling. [#39250](https://github.com/woocommerce/woocommerce/pull/39250) +* Fix - Onboarding payments task not completed after setting up WooPayments [#39786](https://github.com/woocommerce/woocommerce/pull/39786) +* Fix - Prevent possible error when refreshing order edit locks. [#39498](https://github.com/woocommerce/woocommerce/pull/39498) +* Fix - Prevent possible fatal error when edit lock is held on deleted order. [#39497](https://github.com/woocommerce/woocommerce/pull/39497) +* Fix - Store transactional data in order tables with HPOS. [#39381](https://github.com/woocommerce/woocommerce/pull/39381) +* Fix - Support inserting NULL values for strict DB mode for DataBase Util's insert_on_duplicate_key_update method. [#39396](https://github.com/woocommerce/woocommerce/pull/39396) +* Fix - Update CSS prop 'end' to 'flex-end' when using flexbox. [#39419](https://github.com/woocommerce/woocommerce/pull/39419) +* Fix - Use admin theme color for select2, instead of hardcoded theme values. [#39451](https://github.com/woocommerce/woocommerce/pull/39451) +* Fix - Use admin theme color instead of old pink. Update old pink to the new brand color. [#39182](https://github.com/woocommerce/woocommerce/pull/39182) +* Fix - [Product Block Editor] remove digital products from the target list #39769 [#39801](https://github.com/woocommerce/woocommerce/pull/39801) +* Add - Add block template registry and controller [#39698](https://github.com/woocommerce/woocommerce/pull/39698) +* Add - Add delete option to generate variations API, to auto delete unmatched variations. [#39733](https://github.com/woocommerce/woocommerce/pull/39733) +* Add - Added feature flag that removes store appearance task and adds customize store task when enabled [#39397](https://github.com/woocommerce/woocommerce/pull/39397) +* Add - Add filter for adding new user preference option for notice to user data fields. [#39685](https://github.com/woocommerce/woocommerce/pull/39685) +* Add - Add plugin installation request track for core profiler [#39533](https://github.com/woocommerce/woocommerce/pull/39533) +* Add - Add post_password for products for REST API V3 [#39438](https://github.com/woocommerce/woocommerce/pull/39438) +* Add - Add support for Japan and UAE in WooPayments [#39431](https://github.com/woocommerce/woocommerce/pull/39431) +* Add - Add woocommerce/product-password-field block to new product editor [#39464](https://github.com/woocommerce/woocommerce/pull/39464) +* Add - API for block-based templates. [#39470](https://github.com/woocommerce/woocommerce/pull/39470) +* Add - Register product catalog and search visibility blocks [#39477](https://github.com/woocommerce/woocommerce/pull/39477) +* Add - Register the product variation items block [#39657](https://github.com/woocommerce/woocommerce/pull/39657) +* Add - [E2E test coverage]: Disable block product editor #39417 [#39493](https://github.com/woocommerce/woocommerce/pull/39493) +* Add - [E2E test coverage]: Enable new product management experience [#39463](https://github.com/woocommerce/woocommerce/pull/39463) +* Add - [E2E test coverage]: General tab #39411 [#39493](https://github.com/woocommerce/woocommerce/pull/39493) +* Update - Add 'variable' to supported post types for product block editor [#39256](https://github.com/woocommerce/woocommerce/pull/39256) +* Update - Added xstate scaffolding for customize your store feature [#39619](https://github.com/woocommerce/woocommerce/pull/39619) +* Update - add time support to product import on sale dates [#39372](https://github.com/woocommerce/woocommerce/pull/39372) +* Update - On the order confirmation screen, show the 'thank you' message regardless of whether the viewer is verified or not. [#39758](https://github.com/woocommerce/woocommerce/pull/39758) +* Update - Support `first_used` and `installation_date` mobile usage data for WCTracker. [#39605](https://github.com/woocommerce/woocommerce/pull/39605) +* Update - Updates Action Scheduler to 3.6.2 (bug fixes and improvements to help debug problems). [#39665](https://github.com/woocommerce/woocommerce/pull/39665) +* Update - Update task list to show a spinner on item click [#39270](https://github.com/woocommerce/woocommerce/pull/39270) +* Update - Update WCPay banners for WooPay in eligible countries. [#39596](https://github.com/woocommerce/woocommerce/pull/39596) +* Update - Update WooCommerce Blocks to 10.9.0 [#39783](https://github.com/woocommerce/woocommerce/pull/39783) +* Update - Update WooCommerce Blocks to 10.9.2 [#39828](https://github.com/woocommerce/woocommerce/pull/39828) +* Update - Use the same checkbox style on the platform selctor [#39469](https://github.com/woocommerce/woocommerce/pull/39469) +* Dev - Added a unit test for plugin feature compatibility data in WC Tracker [#38931](https://github.com/woocommerce/woocommerce/pull/38931) +* Dev - Added storybook for core profiler pages [#39046](https://github.com/woocommerce/woocommerce/pull/39046) +* Dev - Fixed TS type error for state machine Context in Core Profiler that only got caught after TS5 upgrade [#39749](https://github.com/woocommerce/woocommerce/pull/39749) +* Dev - Fixes a failing e2e test in our daily test runs. [#39674](https://github.com/woocommerce/woocommerce/pull/39674) +* Dev - Fix flaky E2E tests in analytics-overview.spec.js. [#39308](https://github.com/woocommerce/woocommerce/pull/39308) +* Dev - Optimized the System Status Report unit tests. [#39363](https://github.com/woocommerce/woocommerce/pull/39363) +* Dev - Refactored some core profiler utils out to reuse them in customise your store. [#39581](https://github.com/woocommerce/woocommerce/pull/39581) +* Dev - Remove dependency on e2e-environment and e2e-utils in wc-admin. [#39746](https://github.com/woocommerce/woocommerce/pull/39746) +* Dev - Remove the non-existing method from TaskList docs. [#39454](https://github.com/woocommerce/woocommerce/pull/39454) +* Dev - Remove unused variation option components [#39673](https://github.com/woocommerce/woocommerce/pull/39673) +* Dev - Runs all API tests on daily run. Skips failing tests on CI. [#39351](https://github.com/woocommerce/woocommerce/pull/39351) +* Dev - Shard the unit tests into two test suites. [#39362](https://github.com/woocommerce/woocommerce/pull/39362) +* Dev - Simplify user id retrieval in analytics-overview.spec.js. [#39472](https://github.com/woocommerce/woocommerce/pull/39472) +* Dev - Update pnpm to 8.6.7 [#39245](https://github.com/woocommerce/woocommerce/pull/39245) +* Dev - Upgrade TypeScript to 5.1.6 [#39531](https://github.com/woocommerce/woocommerce/pull/39531) +* Dev - [Product Block Editor] Disable tabs in parent product page with variations #39459 [#39675](https://github.com/woocommerce/woocommerce/pull/39675) +* Dev - [Product Block Editor] Disable the new editor for variable products. [#39780](https://github.com/woocommerce/woocommerce/pull/39780) +* Tweak - Add loading indicator when submitting location in Tax task [#39613](https://github.com/woocommerce/woocommerce/pull/39613) +* Tweak - Center align checkbox, logo, and the title on the plugins page (core profiler) [#39394](https://github.com/woocommerce/woocommerce/pull/39394) +* Tweak - Do not run 'woocommerce_process_shop_order_meta' for order post when HPOS is authoritative. [#39587](https://github.com/woocommerce/woocommerce/pull/39587) +* Tweak - Fix TikTok naming. [#39748](https://github.com/woocommerce/woocommerce/pull/39748) +* Tweak - Modified the error message shown to customers in the event that no payment gateways are available. [#39348](https://github.com/woocommerce/woocommerce/pull/39348) +* Tweak - Remove subheading letter-spacing from the core profiler pages. [#39526](https://github.com/woocommerce/woocommerce/pull/39526) +* Tweak - Run A/B test on the core profiler plugins page -- suggest Jetpack or Jetpack Boost [#39799](https://github.com/woocommerce/woocommerce/pull/39799) +* Tweak - Safety measures to prevent future breakages when executing bulk actions in the order list table (HPOS). [#39524](https://github.com/woocommerce/woocommerce/pull/39524) +* Tweak - When all plugins are deselected, but Jetpack is already installed and not connected, redirect users to the Jetpack Connect page. [#39109](https://github.com/woocommerce/woocommerce/pull/39109) +* Tweak - When HPOS is authoritative, execute order update logic earlier during the request. [#39590](https://github.com/woocommerce/woocommerce/pull/39590) +* Performance - Use direct meta calls for backfilling instead of expensive object update. [#39450](https://github.com/woocommerce/woocommerce/pull/39450) +* Enhancement - Add filter `woocommerce_pre_delete_{object_type}` which allows preventing deletion.' [#39650](https://github.com/woocommerce/woocommerce/pull/39650) +* Enhancement - Create the Organization tab [#39232](https://github.com/woocommerce/woocommerce/pull/39232) +* Enhancement - Modify order index to also include date for faster order list query. [#39682](https://github.com/woocommerce/woocommerce/pull/39682) +* Enhancement - Update the admin's menu remaining tasks bubble CSS class and handling [#39273](https://github.com/woocommerce/woocommerce/pull/39273) + + = 8.0.3 2023-08-29 = * Update - Bump WooCommerce Blocks to 10.6.6. [#39853](https://github.com/woocommerce/woocommerce/pull/39853) From ef0aacf2d2cceaaca5d883c40e3f6e4d58bc4cac Mon Sep 17 00:00:00 2001 From: Nima Karimi <73110514+nima-karimi@users.noreply.github.com> Date: Thu, 14 Sep 2023 04:24:19 +0100 Subject: [PATCH 58/59] Woo AI Release 0.4.0 (#40134) Updating version and changelog --- plugins/woo-ai/CHANGELOG.md | 21 +++++++++++++++++++ .../woo-ai/changelog/add-ai-settings-38746 | 4 ---- .../changelog/add-description-branding-39197 | 4 ---- .../add-update-short-description-39151 | 4 ---- plugins/woo-ai/changelog/dev-sync-pnpm | 4 ---- .../woo-ai/changelog/dev-update-pnpm-8.6.5 | 4 ---- .../woo-ai/changelog/dev-update-wp-env-820 | 4 ---- plugins/woo-ai/changelog/dev-upgrade-ts-5 | 4 ---- .../feature-ai-description-product-data-38338 | 4 ---- .../feature-suggest-product-categories-39436 | 4 ---- .../changelog/fix-woo-ai-settings-group | 4 ---- .../woo-ai/changelog/fix-woo-ai-settings-page | 4 ---- .../changelog/fix-woo-ai-settings-toggle | 4 ---- .../changelog/update-completion-hooks-39009 | 4 ---- .../update-short-description-generator | 4 ---- .../changelog/update-woo-ai-endpoints-38895 | 4 ---- plugins/woo-ai/composer.json | 2 +- plugins/woo-ai/package.json | 2 +- plugins/woo-ai/woo-ai.php | 2 +- 19 files changed, 24 insertions(+), 63 deletions(-) delete mode 100644 plugins/woo-ai/changelog/add-ai-settings-38746 delete mode 100644 plugins/woo-ai/changelog/add-description-branding-39197 delete mode 100644 plugins/woo-ai/changelog/add-update-short-description-39151 delete mode 100644 plugins/woo-ai/changelog/dev-sync-pnpm delete mode 100644 plugins/woo-ai/changelog/dev-update-pnpm-8.6.5 delete mode 100644 plugins/woo-ai/changelog/dev-update-wp-env-820 delete mode 100644 plugins/woo-ai/changelog/dev-upgrade-ts-5 delete mode 100644 plugins/woo-ai/changelog/feature-ai-description-product-data-38338 delete mode 100644 plugins/woo-ai/changelog/feature-suggest-product-categories-39436 delete mode 100644 plugins/woo-ai/changelog/fix-woo-ai-settings-group delete mode 100644 plugins/woo-ai/changelog/fix-woo-ai-settings-page delete mode 100644 plugins/woo-ai/changelog/fix-woo-ai-settings-toggle delete mode 100644 plugins/woo-ai/changelog/update-completion-hooks-39009 delete mode 100644 plugins/woo-ai/changelog/update-short-description-generator delete mode 100644 plugins/woo-ai/changelog/update-woo-ai-endpoints-38895 diff --git a/plugins/woo-ai/CHANGELOG.md b/plugins/woo-ai/CHANGELOG.md index 0e1a95f5fbd..eb404e2c2a6 100644 --- a/plugins/woo-ai/CHANGELOG.md +++ b/plugins/woo-ai/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.4](https://github.com/woocommerce/woocommerce/releases/tag/0.4) - 2023-09-12 + +- Patch - Add Woo AI Personalization setting and check setting when generating descriptions with AI. +- Minor - Suggest product categories using AI +- Minor - [Woo AI] Add a Write with AI button for the short description field in product editor. + +## [0.3](https://github.com/woocommerce/woocommerce/releases/tag/0.3) - 2023-08-18 + +- Patch - Fix Woo AI settings page fields persistence bug when disabling the feature. +- Patch - Woo AI - Fix store branding settings retrieval for use with description generation. +- Minor - Adding settings screen for AI centric settings. +- Minor - Generating short description after long description on product editor. +- Minor - [Woo AI] Add Store Branding data to product description generation prompt. +- Minor - Moving text completion hooks into @woocommerce/ai package for reuse. +- Minor - Updating AI endpoints for product editing features. +- Minor - Use additional product data (categories, tags, and attributes) when generating product descriptions. +- Minor - Update pnpm monorepo-wide to 8.6.5 +- Minor - Update pnpm to 8.6.7 +- Patch - Update `wp-env` to version 8.2.0. +- Minor - Upgrade TypeScript to 5.1.6 + ## [0.2](https://github.com/woocommerce/woocommerce/releases/tag/0.2) - 2023-06-28 - Minor - Adding error handling for a bad token request. diff --git a/plugins/woo-ai/changelog/add-ai-settings-38746 b/plugins/woo-ai/changelog/add-ai-settings-38746 deleted file mode 100644 index 1c8d3b40262..00000000000 --- a/plugins/woo-ai/changelog/add-ai-settings-38746 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Adding settings screen for AI centric settings. diff --git a/plugins/woo-ai/changelog/add-description-branding-39197 b/plugins/woo-ai/changelog/add-description-branding-39197 deleted file mode 100644 index 10cf10f2f3b..00000000000 --- a/plugins/woo-ai/changelog/add-description-branding-39197 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -[Woo AI] Add Store Branding data to product description generation prompt. diff --git a/plugins/woo-ai/changelog/add-update-short-description-39151 b/plugins/woo-ai/changelog/add-update-short-description-39151 deleted file mode 100644 index 1f6c9d941d7..00000000000 --- a/plugins/woo-ai/changelog/add-update-short-description-39151 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Generating short description after long description on product editor. diff --git a/plugins/woo-ai/changelog/dev-sync-pnpm b/plugins/woo-ai/changelog/dev-sync-pnpm deleted file mode 100644 index 2e1c2db5705..00000000000 --- a/plugins/woo-ai/changelog/dev-sync-pnpm +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update pnpm to 8.6.7 diff --git a/plugins/woo-ai/changelog/dev-update-pnpm-8.6.5 b/plugins/woo-ai/changelog/dev-update-pnpm-8.6.5 deleted file mode 100644 index 601ddf2c1c8..00000000000 --- a/plugins/woo-ai/changelog/dev-update-pnpm-8.6.5 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Update pnpm monorepo-wide to 8.6.5 diff --git a/plugins/woo-ai/changelog/dev-update-wp-env-820 b/plugins/woo-ai/changelog/dev-update-wp-env-820 deleted file mode 100644 index f121d5cf4f0..00000000000 --- a/plugins/woo-ai/changelog/dev-update-wp-env-820 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: dev - -Update `wp-env` to version 8.2.0. diff --git a/plugins/woo-ai/changelog/dev-upgrade-ts-5 b/plugins/woo-ai/changelog/dev-upgrade-ts-5 deleted file mode 100644 index 0785aabfbbf..00000000000 --- a/plugins/woo-ai/changelog/dev-upgrade-ts-5 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: dev - -Upgrade TypeScript to 5.1.6 diff --git a/plugins/woo-ai/changelog/feature-ai-description-product-data-38338 b/plugins/woo-ai/changelog/feature-ai-description-product-data-38338 deleted file mode 100644 index 2ae7aa51055..00000000000 --- a/plugins/woo-ai/changelog/feature-ai-description-product-data-38338 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Use additional product data (categories, tags, and attributes) when generating product descriptions. diff --git a/plugins/woo-ai/changelog/feature-suggest-product-categories-39436 b/plugins/woo-ai/changelog/feature-suggest-product-categories-39436 deleted file mode 100644 index f5e83c485f4..00000000000 --- a/plugins/woo-ai/changelog/feature-suggest-product-categories-39436 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -Suggest product categories using AI diff --git a/plugins/woo-ai/changelog/fix-woo-ai-settings-group b/plugins/woo-ai/changelog/fix-woo-ai-settings-group deleted file mode 100644 index 8816613878c..00000000000 --- a/plugins/woo-ai/changelog/fix-woo-ai-settings-group +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Woo AI - Fix store branding settings retrieval for use with description generation. diff --git a/plugins/woo-ai/changelog/fix-woo-ai-settings-page b/plugins/woo-ai/changelog/fix-woo-ai-settings-page deleted file mode 100644 index 56aaea09869..00000000000 --- a/plugins/woo-ai/changelog/fix-woo-ai-settings-page +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Fix Woo AI settings page fields persistence bug when disabling the feature. diff --git a/plugins/woo-ai/changelog/fix-woo-ai-settings-toggle b/plugins/woo-ai/changelog/fix-woo-ai-settings-toggle deleted file mode 100644 index 5f0bf0d196e..00000000000 --- a/plugins/woo-ai/changelog/fix-woo-ai-settings-toggle +++ /dev/null @@ -1,4 +0,0 @@ -Significance: patch -Type: fix - -Add Woo AI Personalization setting and check setting when generating descriptions with AI. diff --git a/plugins/woo-ai/changelog/update-completion-hooks-39009 b/plugins/woo-ai/changelog/update-completion-hooks-39009 deleted file mode 100644 index d634daf8002..00000000000 --- a/plugins/woo-ai/changelog/update-completion-hooks-39009 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Moving text completion hooks into @woocommerce/ai package for reuse. diff --git a/plugins/woo-ai/changelog/update-short-description-generator b/plugins/woo-ai/changelog/update-short-description-generator deleted file mode 100644 index ab2b75fdcf4..00000000000 --- a/plugins/woo-ai/changelog/update-short-description-generator +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: add - -[Woo AI] Add a Write with AI button for the short description field in product editor. diff --git a/plugins/woo-ai/changelog/update-woo-ai-endpoints-38895 b/plugins/woo-ai/changelog/update-woo-ai-endpoints-38895 deleted file mode 100644 index f812afa2f65..00000000000 --- a/plugins/woo-ai/changelog/update-woo-ai-endpoints-38895 +++ /dev/null @@ -1,4 +0,0 @@ -Significance: minor -Type: update - -Updating AI endpoints for product editing features. diff --git a/plugins/woo-ai/composer.json b/plugins/woo-ai/composer.json index c660e4c15c0..12de81c321e 100644 --- a/plugins/woo-ai/composer.json +++ b/plugins/woo-ai/composer.json @@ -6,7 +6,7 @@ "license": "GPL-3.0-or-later", "prefer-stable": true, "minimum-stability": "dev", - "version": "0.2.0", + "version": "0.4.0", "require": { "composer/installers": "~1.7", "ext-json": "*" diff --git a/plugins/woo-ai/package.json b/plugins/woo-ai/package.json index 6f2c4e98af6..c08f85a2179 100644 --- a/plugins/woo-ai/package.json +++ b/plugins/woo-ai/package.json @@ -7,7 +7,7 @@ "url": "git://github.com/woocommerce/woo-ai.git" }, "title": "Woo AI", - "version": "0.2.0", + "version": "0.4.0", "homepage": "http://github.com/woocommerce/woo-ai", "devDependencies": { "@svgr/webpack": "^8.1.0", diff --git a/plugins/woo-ai/woo-ai.php b/plugins/woo-ai/woo-ai.php index a8696d63af0..31dba018270 100644 --- a/plugins/woo-ai/woo-ai.php +++ b/plugins/woo-ai/woo-ai.php @@ -3,7 +3,7 @@ * Plugin Name: Woo AI * Plugin URI: https://github.com/woocommerce/woocommerce/ * Description: Enable AI experiments within the WooCommerce experience. Learn more. - * Version: 0.2.0 + * Version: 0.4.0 * Author: WooCommerce * Author URI: http://woocommerce.com/ * Requires at least: 5.8 From 473a53d54243c6b749a4532112eea4ac8667447f Mon Sep 17 00:00:00 2001 From: Jonathan Lane Date: Thu, 14 Sep 2023 00:10:20 -0700 Subject: [PATCH 59/59] Add test to check required fields on checkout (#40099) * Add test to check required fields on checkout * Add changelog --------- Co-authored-by: Jon Lane --- .../changelog/e2e-shopper-checkout | 4 ++ .../e2e-pw/tests/shopper/checkout.spec.js | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 plugins/woocommerce/changelog/e2e-shopper-checkout diff --git a/plugins/woocommerce/changelog/e2e-shopper-checkout b/plugins/woocommerce/changelog/e2e-shopper-checkout new file mode 100644 index 00000000000..43e37f2916f --- /dev/null +++ b/plugins/woocommerce/changelog/e2e-shopper-checkout @@ -0,0 +1,4 @@ +Significance: patch +Type: dev + +Adds test to check required fields on checkout diff --git a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js index 75195a3ed48..d06b1f95820 100644 --- a/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js +++ b/plugins/woocommerce/tests/e2e-pw/tests/shopper/checkout.spec.js @@ -178,6 +178,45 @@ test.describe( 'Checkout page', () => { await expect( page.locator( '#billing_email' ) ).toBeEditable(); } ); + test( 'warn when customer is missing required details', async ( { page } ) => { + await page.goto( `/shop/?add-to-cart=${ productId }`, { waitUntil: 'networkidle' } ); + + await page.goto( '/checkout/' ); + + // first try submitting the form with no fields complete + await page.getByRole('button', { name: 'Place order' }).click(); + await expect( page.locator( 'ul.woocommerce-error' ) ).toBeVisible(); + await expect( page.getByText( 'Billing First name is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Billing Last name is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Billing Street address is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Billing Town / City is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Billing ZIP Code is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Billing Phone is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Billing Email address is a required field.' ) ).toBeVisible(); + + // toggle ship to different address, fill out billing info and confirm error shown + await page.getByText('Ship to a different address?').click(); + await page.locator( '#billing_first_name' ).fill( 'Homer' ); + await page.locator( '#billing_last_name' ).fill( 'Simpson' ); + await page + .locator( '#billing_address_1' ) + .fill( '123 Evergreen Terrace' ); + await page.locator( '#billing_city' ).fill( 'Springfield' ); + await page.locator( '#billing_country' ).selectOption( 'US' ); + await page.locator( '#billing_state' ).selectOption( 'OR' ); + await page.locator( '#billing_postcode' ).fill( '97403' ); + await page.locator( '#billing_phone' ).fill( '555 555-5555' ); + await page.locator( '#billing_email' ).fill( customer.email ); + await page.getByRole('button', { name: 'Place order' }).click(); + + await expect( page.locator( 'ul.woocommerce-error' ) ).toBeVisible(); + await expect( page.getByText( 'Shipping First name is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Shipping Last name is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Shipping Street address is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Shipping Town / City is a required field.' ) ).toBeVisible(); + await expect( page.getByText( 'Shipping ZIP Code is a required field.' ) ).toBeVisible(); + } ); + test( 'allows customer to fill shipping details', async ( { page } ) => { for ( let i = 1; i < 3; i++ ) { await page.goto( `/shop/?add-to-cart=${ productId }` );