Merge branch 'trunk' into e2e/remove-obw-tests
# Conflicts: # plugins/woocommerce/tests/e2e-pw/tests/activate-and-setup/complete-onboarding-wizard.spec.js
This commit is contained in:
commit
6458ad4657
175
.github/actions/tests/slack-summary-daily/scripts/construct-slack-payload.js
vendored
Normal file
175
.github/actions/tests/slack-summary-daily/scripts/construct-slack-payload.js
vendored
Normal file
|
@ -0,0 +1,175 @@
|
|||
module.exports = async ( { context, core, github } ) => {
|
||||
const {
|
||||
API_RESULT,
|
||||
E2E_RESULT,
|
||||
k6_RESULT,
|
||||
PLUGINS_BLOCKS_PATH,
|
||||
PLUGIN_TESTS_RESULT,
|
||||
GITHUB_REF_NAME,
|
||||
GITHUB_RUN_ID,
|
||||
} = process.env;
|
||||
const {
|
||||
selectEmoji,
|
||||
readContextBlocksFromJsonFiles,
|
||||
} = require( './utils' );
|
||||
|
||||
const URL_GITHUB_RUN_LOG = `https://github.com/woocommerce/woocommerce/actions/runs/${ GITHUB_RUN_ID }`;
|
||||
|
||||
const create_blockGroup_header = async () => {
|
||||
const getRunStartDate = async () => {
|
||||
const response = await github.rest.actions.getWorkflowRun( {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: GITHUB_RUN_ID,
|
||||
} );
|
||||
const runStartedAt = new Date( response.data.run_started_at );
|
||||
const intlDateTimeFormatOptions = {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'long',
|
||||
};
|
||||
const date = new Intl.DateTimeFormat(
|
||||
'en-US',
|
||||
intlDateTimeFormatOptions
|
||||
).format( runStartedAt );
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
const readableDate = await getRunStartDate();
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'Daily test results',
|
||||
emoji: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `*Run started:* ${ readableDate }`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `*Branch:* \`${ GITHUB_REF_NAME }\``,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `*GitHub run logs:* <${ URL_GITHUB_RUN_LOG }|${ GITHUB_RUN_ID }>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: '*Test reports dashboard:* <https://woocommerce.github.io/woocommerce-test-reports/daily/|Daily smoke tests>',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
];
|
||||
|
||||
return blocks;
|
||||
};
|
||||
|
||||
const create_blockGroup_nightlySite = () => {
|
||||
const emoji_API = selectEmoji( API_RESULT );
|
||||
const emoji_E2E = selectEmoji( E2E_RESULT );
|
||||
const emoji_k6 = selectEmoji( k6_RESULT );
|
||||
const url_API =
|
||||
'https://woocommerce.github.io/woocommerce-test-reports/daily/nightly-site/api';
|
||||
const url_E2E =
|
||||
'https://woocommerce.github.io/woocommerce-test-reports/daily/nightly-site/e2e';
|
||||
const url_k6 = URL_GITHUB_RUN_LOG;
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `<${ URL_GITHUB_RUN_LOG }|*Smoke tests on daily build*>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `<${ url_API }|API> ${ emoji_API }\t<${ url_E2E }|E2E> ${ emoji_E2E }\t<${ url_k6 }|k6> ${ emoji_k6 }`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
];
|
||||
|
||||
return blocks;
|
||||
};
|
||||
|
||||
const create_blockGroups_plugins = () => {
|
||||
const pluginTestsSkipped = PLUGIN_TESTS_RESULT === 'skipped';
|
||||
const blocks_pluginTestsSkipped = [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: ':warning: *Plugin tests were not run!*',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `Head over to the <${ URL_GITHUB_RUN_LOG }|GitHub workflow run log> to see what went wrong.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
];
|
||||
|
||||
return pluginTestsSkipped
|
||||
? blocks_pluginTestsSkipped
|
||||
: readContextBlocksFromJsonFiles( PLUGINS_BLOCKS_PATH );
|
||||
};
|
||||
|
||||
const blockGroup_header = await create_blockGroup_header();
|
||||
const blockGroup_nightlySite = create_blockGroup_nightlySite();
|
||||
const blockGroups_plugins = create_blockGroups_plugins();
|
||||
const blocks_all = [
|
||||
...blockGroup_header,
|
||||
...blockGroup_nightlySite,
|
||||
...blockGroups_plugins.flat(),
|
||||
];
|
||||
const payload = {
|
||||
text: 'Daily test results',
|
||||
blocks: blocks_all,
|
||||
};
|
||||
const payload_stringified = JSON.stringify( payload );
|
||||
|
||||
core.setOutput( 'payload', payload_stringified );
|
||||
};
|
37
.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js
vendored
Normal file
37
.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
module.exports = ( { core } ) => {
|
||||
const { UPLOAD_RESULT, E2E_RESULT, PLUGIN_NAME, PLUGIN_SLUG } = process.env;
|
||||
const { selectEmoji } = require( './utils' );
|
||||
const fs = require( 'fs' );
|
||||
|
||||
const emoji_UPLOAD = selectEmoji( UPLOAD_RESULT );
|
||||
const emoji_E2E = selectEmoji( E2E_RESULT );
|
||||
const reportURL = `https://woocommerce.github.io/woocommerce-test-reports/daily/${ PLUGIN_SLUG }/e2e`;
|
||||
|
||||
const blockGroup = [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `<${ reportURL }|*${ PLUGIN_NAME }*>`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'context',
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `"Upload plugin" test ${ emoji_UPLOAD }\tOther E2E tests ${ emoji_E2E }`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
];
|
||||
const blockGroup_stringified = JSON.stringify( blockGroup );
|
||||
|
||||
const path = `/tmp/${ PLUGIN_SLUG }.json`;
|
||||
fs.writeFileSync( path, blockGroup_stringified );
|
||||
|
||||
core.setOutput( 'path', path );
|
||||
};
|
26
.github/actions/tests/slack-summary-daily/scripts/utils/get-context-blocks.js
vendored
Normal file
26
.github/actions/tests/slack-summary-daily/scripts/utils/get-context-blocks.js
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
const fs = require( 'fs' );
|
||||
const path = require( 'path' );
|
||||
|
||||
/**
|
||||
* @param {string} blocksDir
|
||||
* @returns {any[][]}
|
||||
*/
|
||||
const readContextBlocksFromJsonFiles = ( blocksDir ) => {
|
||||
const jsonsDir = path.resolve( blocksDir );
|
||||
const jsons = fs.readdirSync( jsonsDir );
|
||||
|
||||
let contextBlocks = [];
|
||||
|
||||
for ( const json of jsons ) {
|
||||
const jsonPath = path.resolve( jsonsDir, json );
|
||||
const contextBlock = require( jsonPath );
|
||||
|
||||
contextBlocks.push( contextBlock );
|
||||
}
|
||||
|
||||
return contextBlocks;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
readContextBlocksFromJsonFiles,
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
const { readContextBlocksFromJsonFiles } = require( './get-context-blocks' );
|
||||
const { selectEmoji } = require( './select-emoji' );
|
||||
|
||||
module.exports = {
|
||||
readContextBlocksFromJsonFiles,
|
||||
selectEmoji,
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
const emojis = {
|
||||
PASSED: ':workflow-passed:',
|
||||
FAILED: ':workflow-failed:',
|
||||
SKIPPED: ':workflow-skipped:',
|
||||
CANCELLED: ':workflow-cancelled:',
|
||||
UNKNOWN: ':grey_question:',
|
||||
};
|
||||
|
||||
const selectEmoji = ( result ) => {
|
||||
switch ( result ) {
|
||||
case 'success':
|
||||
return emojis.PASSED;
|
||||
case 'failure':
|
||||
return emojis.FAILED;
|
||||
case 'skipped':
|
||||
return emojis.SKIPPED;
|
||||
case 'cancelled':
|
||||
return emojis.CANCELLED;
|
||||
default:
|
||||
return emojis.UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
selectEmoji,
|
||||
};
|
|
@ -8,6 +8,7 @@ env:
|
|||
API_ARTIFACT: api-daily--run-${{ github.run_number }}
|
||||
E2E_ARTIFACT: e2e-daily--run-${{ github.run_number }}
|
||||
FORCE_COLOR: 1
|
||||
PLUGIN_SLACK_BLOCKS_ARTIFACT: plugin-blocks
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
@ -21,6 +22,8 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
test-result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report
|
||||
|
@ -42,38 +45,31 @@ jobs:
|
|||
install-filters: woocommerce
|
||||
build: false
|
||||
|
||||
- name: Download and install Chromium browser.
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec playwright install chromium
|
||||
- name: Update site to nightly version
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
report-name: ${{ env.API_ARTIFACT }}
|
||||
tests: update-woocommerce.spec.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||
UPDATE_WC: nightly
|
||||
|
||||
- name: Run API tests.
|
||||
working-directory: plugins/woocommerce
|
||||
- name: Run API tests
|
||||
id: run-api-composite-action
|
||||
uses: ./.github/actions/tests/run-api-tests
|
||||
with:
|
||||
report-name: ${{ env.API_ARTIFACT }}
|
||||
env:
|
||||
USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
|
||||
USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
||||
run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js
|
||||
|
||||
- name: Generate API Test report.
|
||||
if: success() || failure()
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
|
||||
|
||||
- name: Archive API test report
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.API_ARTIFACT }}
|
||||
path: |
|
||||
${{ env.ALLURE_RESULTS_DIR }}
|
||||
${{ env.ALLURE_REPORT_DIR }}
|
||||
if-no-files-found: ignore
|
||||
retention-days: 5
|
||||
|
||||
e2e-tests:
|
||||
name: E2E tests on nightly build
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
test-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
# needs: [api-tests]
|
||||
env:
|
||||
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
||||
|
@ -85,7 +81,6 @@ jobs:
|
|||
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
|
||||
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
|
||||
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
@ -95,33 +90,24 @@ jobs:
|
|||
install-filters: woocommerce
|
||||
build: false
|
||||
|
||||
- name: Download and install Chromium browser.
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Run E2E tests.
|
||||
- name: Run E2E tests
|
||||
id: run-e2e-composite-action
|
||||
timeout-minutes: 60
|
||||
working-directory: plugins/woocommerce
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
report-name: ${{ env.E2E_ARTIFACT }}
|
||||
env:
|
||||
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
||||
ADMIN_USER: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
|
||||
ADMIN_USER_EMAIL: ${{ secrets.SMOKE_TEST_ADMIN_USER_EMAIL }}
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results
|
||||
BASE_URL: ${{ secrets.SMOKE_TEST_URL }}
|
||||
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
|
||||
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
|
||||
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
||||
E2E_MAX_FAILURES: 25
|
||||
RESET_SITE: true
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
|
||||
|
||||
- name: Generate Playwright E2E Test report.
|
||||
if: success() || failure()
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
|
||||
|
||||
- name: Archive E2E test report
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.E2E_ARTIFACT }}
|
||||
path: |
|
||||
${{ env.ALLURE_RESULTS_DIR }}
|
||||
${{ env.ALLURE_REPORT_DIR }}
|
||||
if-no-files-found: ignore
|
||||
retention-days: 5
|
||||
|
||||
k6-tests:
|
||||
name: k6 tests on nightly build
|
||||
|
@ -130,6 +116,8 @@ jobs:
|
|||
contents: read
|
||||
needs: [api-tests]
|
||||
if: success() || failure()
|
||||
outputs:
|
||||
test-result: ${{ steps.run-k6-tests.conclusion }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
@ -139,29 +127,31 @@ jobs:
|
|||
install-filters: woocommerce
|
||||
build: false
|
||||
|
||||
- name: Download and install Chromium browser.
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Update performance test site with E2E test
|
||||
working-directory: plugins/woocommerce
|
||||
id: update-perf-site
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
report-name: k6-daily-update-site--run-${{ github.run_number }}
|
||||
tests: update-woocommerce.spec.js
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
|
||||
BASE_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/
|
||||
ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
||||
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
||||
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
||||
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
||||
UPDATE_WC: nightly
|
||||
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
||||
run: |
|
||||
pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js
|
||||
continue-on-error: true
|
||||
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||
UPDATE_WC: nightly
|
||||
|
||||
- name: Install k6
|
||||
run: |
|
||||
curl https://github.com/grafana/k6/releases/download/v0.33.0/k6-v0.33.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1
|
||||
|
||||
- name: Run k6 smoke tests
|
||||
id: run-k6-tests
|
||||
env:
|
||||
URL: ${{ secrets.SMOKE_TEST_PERF_URL }}
|
||||
HOST: ${{ secrets.SMOKE_TEST_PERF_HOST }}
|
||||
|
@ -180,7 +170,6 @@ jobs:
|
|||
contents: read
|
||||
needs: [api-tests]
|
||||
env:
|
||||
USE_WP_ENV: 1
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
|
||||
strategy:
|
||||
|
@ -189,17 +178,23 @@ jobs:
|
|||
include:
|
||||
- plugin: 'WooCommerce Payments'
|
||||
repo: 'automattic/woocommerce-payments'
|
||||
slug: woocommerce-payments
|
||||
- plugin: 'WooCommerce PayPal Payments'
|
||||
repo: 'woocommerce/woocommerce-paypal-payments'
|
||||
slug: woocommerce-paypal-payments
|
||||
- plugin: 'WooCommerce Shipping & Tax'
|
||||
repo: 'automattic/woocommerce-services'
|
||||
slug: woocommerce-services
|
||||
- plugin: 'WooCommerce Subscriptions'
|
||||
repo: WC_SUBSCRIPTIONS_REPO
|
||||
private: true
|
||||
slug: woocommerce-subscriptions
|
||||
- plugin: 'Gutenberg'
|
||||
repo: 'WordPress/gutenberg'
|
||||
slug: gutenberg
|
||||
- plugin: 'Gutenberg - Nightly'
|
||||
repo: 'bph/gutenberg'
|
||||
slug: gutenberg-nightly
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
@ -208,43 +203,52 @@ jobs:
|
|||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Launch wp-env e2e environment
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm env:test --filter=woocommerce
|
||||
|
||||
- name: Download and install Chromium browser.
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec playwright install chromium
|
||||
- name: Setup local test environment
|
||||
uses: ./.github/actions/tests/setup-local-test-environment
|
||||
with:
|
||||
test-type: e2e
|
||||
|
||||
- name: Run 'Upload plugin' test
|
||||
working-directory: plugins/woocommerce
|
||||
id: run-upload-plugin-test
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
report-name: Smoke tests on trunk with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }})
|
||||
tests: upload-plugin.spec.js
|
||||
env:
|
||||
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
|
||||
PLUGIN_NAME: ${{ matrix.plugin }}
|
||||
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||
run: pnpm test:e2e-pw upload-plugin.spec.js
|
||||
|
||||
- name: Run the rest of E2E tests
|
||||
working-directory: plugins/woocommerce
|
||||
id: run-e2e-composite-action
|
||||
timeout-minutes: 60
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
with:
|
||||
playwright-config: ignore-plugin-tests.playwright.config.js
|
||||
report-name: Smoke tests on trunk with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }})
|
||||
env:
|
||||
E2E_MAX_FAILURES: 15
|
||||
run: pnpm test:e2e-pw
|
||||
|
||||
- name: Generate E2E Test report.
|
||||
- name: Create context block and save as JSON file
|
||||
if: success() || failure()
|
||||
working-directory: plugins/woocommerce
|
||||
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
|
||||
id: create-block-json
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const script = require( './.github/actions/tests/slack-summary-daily/scripts/create-blocks-plugin-tests.js' )
|
||||
script( { core } );
|
||||
env:
|
||||
UPLOAD_RESULT: ${{ steps.run-upload-plugin-test.outputs.result }}
|
||||
E2E_RESULT: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||
PLUGIN_NAME: ${{ matrix.plugin }}
|
||||
PLUGIN_SLUG: ${{ matrix.slug }}
|
||||
|
||||
- name: Archive E2E test report
|
||||
- name: Upload JSON file as artifact
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Smoke tests on trunk with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }})
|
||||
path: |
|
||||
${{ env.ALLURE_RESULTS_DIR }}
|
||||
${{ env.ALLURE_REPORT_DIR }}
|
||||
if-no-files-found: ignore
|
||||
retention-days: 5
|
||||
name: ${{ env.PLUGIN_SLACK_BLOCKS_ARTIFACT }}
|
||||
path: ${{ steps.create-block-json.outputs.path }}
|
||||
|
||||
trunk-results:
|
||||
name: Publish report on smoke tests on nightly build
|
||||
|
@ -306,7 +310,8 @@ jobs:
|
|||
plugins-results:
|
||||
name: Publish report on Smoke tests on trunk with plugins
|
||||
if: |
|
||||
( success() || failure() ) &&
|
||||
( success() || failure() ) &&
|
||||
( needs.test-plugins.result != 'skipped' ) &&
|
||||
! github.event.pull_request.head.repo.fork
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [e2e-tests, test-plugins, k6-tests]
|
||||
|
@ -345,3 +350,51 @@ jobs:
|
|||
-f slug="${{ matrix.slug }}" \
|
||||
-f s3_root=public \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
post-slack-summary:
|
||||
name: Post Slack summary
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
if: |
|
||||
success() || (
|
||||
failure() && contains( needs.*.result, 'failure' )
|
||||
)
|
||||
needs:
|
||||
- api-tests
|
||||
- e2e-tests
|
||||
- k6-tests
|
||||
- test-plugins
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Slack blocks from plugin tests
|
||||
if: needs.test-plugins.result != 'skipped'
|
||||
id: download-plugin-blocks
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.PLUGIN_SLACK_BLOCKS_ARTIFACT }}
|
||||
path: /tmp/plugin-blocks
|
||||
|
||||
- name: Construct Slack payload
|
||||
id: construct-slack-payload
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/actions/tests/slack-summary-daily/scripts/construct-slack-payload.js');
|
||||
await script( { context, core, github } );
|
||||
env:
|
||||
API_RESULT: ${{ needs.api-tests.outputs.test-result }}
|
||||
E2E_RESULT: ${{ needs.e2e-tests.outputs.test-result || needs.e2e-tests.result }}
|
||||
k6_RESULT: ${{ needs.k6-tests.outputs.test-result || needs.k6-tests.result }}
|
||||
PLUGINS_BLOCKS_PATH: ${{ steps.download-plugin-blocks.outputs.download-path }}
|
||||
PLUGIN_TESTS_RESULT: ${{ needs.test-plugins.result }}
|
||||
|
||||
- name: Send Slack message
|
||||
id: send-slack-message
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
with:
|
||||
channel-id: ${{ secrets.DAILY_TEST_SLACK_CHANNEL }}
|
||||
payload: ${{ steps.construct-slack-payload.outputs.payload }}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.E2E_SLACK_TOKEN }}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Decode html characters in SelectTree
|
||||
|
||||
|
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
|||
import { createElement, useEffect, useState } from '@wordpress/element';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { BaseControl, TextControl } from '@wordpress/components';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -16,6 +17,7 @@ import { SelectedItems } from '../experimental-select-control/selected-items';
|
|||
import { ComboBox } from '../experimental-select-control/combo-box';
|
||||
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
|
||||
import { SelectTreeMenu } from './select-tree-menu';
|
||||
import { escapeHTML } from '../utils';
|
||||
|
||||
interface SelectTreeProps extends TreeControlProps {
|
||||
id: string;
|
||||
|
@ -185,11 +187,11 @@ export const SelectTree = function SelectTree( {
|
|||
) : (
|
||||
<TextControl
|
||||
{ ...inputProps }
|
||||
value={ props.createValue || '' }
|
||||
value={ decodeEntities( props.createValue || '' ) }
|
||||
onChange={ ( value ) => {
|
||||
if ( onInputChange ) onInputChange( value );
|
||||
const item = items.find(
|
||||
( i ) => i.label === value
|
||||
( i ) => i.label === escapeHTML( value )
|
||||
);
|
||||
if ( props.onSelect && item ) {
|
||||
props.onSelect( item );
|
||||
|
|
|
@ -84,7 +84,7 @@ export { DynamicForm } from './dynamic-form';
|
|||
export { default as TourKit } from './tour-kit';
|
||||
export * as TourKitTypes from './tour-kit/types';
|
||||
export { CollapsibleContent } from './collapsible-content';
|
||||
export { createOrderedChildren, sortFillsByOrder } from './utils';
|
||||
export { createOrderedChildren, sortFillsByOrder, escapeHTML } from './utils';
|
||||
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
|
||||
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';
|
||||
export { WooProductTabItem as __experimentalWooProductTabItem } from './woo-product-tab-item';
|
||||
|
|
|
@ -85,3 +85,10 @@ export const sortFillsByOrder: Slot.Props[ 'children' ] = ( fills ) => {
|
|||
|
||||
return <Fragment>{ sortedFills }</Fragment>;
|
||||
};
|
||||
|
||||
export const escapeHTML = ( string: string ) => {
|
||||
return string
|
||||
.replace( /&/g, '&' )
|
||||
.replace( />/g, '>' )
|
||||
.replace( /</g, '<' );
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add additional type to user preferences config.
|
|
@ -29,10 +29,12 @@ export type UserPreferences = {
|
|||
variable_product_block_tour_shown?: string;
|
||||
variations_report_columns?: string;
|
||||
product_block_variable_options_notice_dismissed?: string;
|
||||
variable_items_without_price_notice_dismissed?: Record< number, string >;
|
||||
};
|
||||
|
||||
export type WoocommerceMeta = UserPreferences & {
|
||||
task_list_tracked_started_tasks?: string;
|
||||
variable_items_without_price_notice_dismissed?: string;
|
||||
};
|
||||
|
||||
export type WCUser<
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add notice to variations when variations do not have prices set.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix error displaying block after removing variation #40255
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add redirection to product edit page if variations are added before it being saved.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Escape special characters when searching for taxonomies
|
||||
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import { escapeHTML } from '@woocommerce/components';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -61,7 +62,7 @@ const useTaxonomySearch = (
|
|||
Taxonomy[]
|
||||
>( 'taxonomy', taxonomyName, {
|
||||
per_page: PAGINATION_SIZE,
|
||||
search,
|
||||
search: escapeHTML( search ),
|
||||
} );
|
||||
if ( options?.fetchParents ) {
|
||||
taxonomies = await getTaxonomiesMissingParents(
|
||||
|
|
|
@ -1,16 +1,32 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
Product,
|
||||
ProductVariation,
|
||||
useUserPreferences,
|
||||
} from '@woocommerce/data';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { createElement, useMemo, useRef } from '@wordpress/element';
|
||||
import { resolveSelect, useDispatch, useSelect } from '@wordpress/data';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { useEntityId, useEntityProp } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { VariationsTable } from '../../components/variations-table';
|
||||
import { useValidation } from '../../contexts/validation-context';
|
||||
import { VariationOptionsBlockAttributes } from './types';
|
||||
import { VariableProductTour } from './variable-product-tour';
|
||||
import { TRACKS_SOURCE } from '../../constants';
|
||||
import { handlePrompt } from '../../utils/handle-prompt';
|
||||
|
||||
export function Edit( {
|
||||
context,
|
||||
|
@ -19,11 +35,168 @@ export function Edit( {
|
|||
isInSelectedTab?: boolean;
|
||||
};
|
||||
} ) {
|
||||
const noticeDimissed = useRef( false );
|
||||
const { invalidateResolution } = useDispatch(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
const productId = useEntityId( 'postType', 'product' );
|
||||
const blockProps = useBlockProps();
|
||||
const [ productStatus ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'status'
|
||||
);
|
||||
|
||||
const totalCountWithoutPriceRequestParams = useMemo(
|
||||
() => ( {
|
||||
product_id: productId,
|
||||
order: 'asc',
|
||||
orderby: 'menu_order',
|
||||
has_price: false,
|
||||
} ),
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const { totalCountWithoutPrice } = useSelect(
|
||||
( select ) => {
|
||||
const { getProductVariationsTotalCount } = select(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
totalCountWithoutPrice:
|
||||
getProductVariationsTotalCount< number >(
|
||||
totalCountWithoutPriceRequestParams
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const {
|
||||
updateUserPreferences,
|
||||
variable_items_without_price_notice_dismissed:
|
||||
itemsWithoutPriceNoticeDismissed,
|
||||
} = useUserPreferences();
|
||||
|
||||
const { ref: variationTableRef } = useValidation< Product >(
|
||||
`variations`,
|
||||
async function regularPriceValidator( defaultValue, newData ) {
|
||||
/**
|
||||
* We cause a validation error if there is:
|
||||
* - more then one variation without a price.
|
||||
* - the notice hasn't been dismissed.
|
||||
* - The product hasn't already been published.
|
||||
* - We are publishing the product.
|
||||
*/
|
||||
if (
|
||||
totalCountWithoutPrice > 0 &&
|
||||
! noticeDimissed.current &&
|
||||
productStatus !== 'publish' &&
|
||||
// New status.
|
||||
newData?.status === 'publish'
|
||||
) {
|
||||
if ( itemsWithoutPriceNoticeDismissed !== 'yes' ) {
|
||||
updateUserPreferences( {
|
||||
variable_items_without_price_notice_dismissed: {
|
||||
...( itemsWithoutPriceNoticeDismissed || {} ),
|
||||
[ productId ]: 'no',
|
||||
},
|
||||
} );
|
||||
}
|
||||
return __(
|
||||
'Set variation prices before adding this product.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
},
|
||||
[ totalCountWithoutPrice ]
|
||||
);
|
||||
|
||||
function onSetPrices(
|
||||
handleUpdateAll: ( update: Partial< ProductVariation >[] ) => void
|
||||
) {
|
||||
recordEvent( 'product_variations_set_prices_select', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
const productVariationsListPromise = resolveSelect(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
).getProductVariations< Pick< ProductVariation, 'id' >[] >( {
|
||||
product_id: productId,
|
||||
has_price: false,
|
||||
_fields: [ 'id' ],
|
||||
} );
|
||||
handlePrompt( {
|
||||
onOk( value ) {
|
||||
recordEvent( 'product_variations_set_prices_update', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
productVariationsListPromise.then( ( variations ) => {
|
||||
handleUpdateAll(
|
||||
variations.map( ( { id } ) => ( {
|
||||
id,
|
||||
regular_price: value,
|
||||
} ) )
|
||||
);
|
||||
} );
|
||||
},
|
||||
} );
|
||||
}
|
||||
|
||||
const hasNotDismissedNotice =
|
||||
! itemsWithoutPriceNoticeDismissed ||
|
||||
itemsWithoutPriceNoticeDismissed[ productId ] !== 'yes';
|
||||
const noticeText =
|
||||
totalCountWithoutPrice > 0 && hasNotDismissedNotice
|
||||
? sprintf(
|
||||
/** Translators: Number of variations without price */
|
||||
__(
|
||||
'%d variations do not have prices. Variations that do not have prices will not be visible to customers.',
|
||||
'woocommerce'
|
||||
),
|
||||
totalCountWithoutPrice
|
||||
)
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<VariationsTable />
|
||||
<VariationsTable
|
||||
ref={ variationTableRef as React.Ref< HTMLDivElement > }
|
||||
noticeText={ noticeText }
|
||||
onNoticeDismiss={ () => {
|
||||
noticeDimissed.current = true;
|
||||
updateUserPreferences( {
|
||||
variable_items_without_price_notice_dismissed: {
|
||||
...( itemsWithoutPriceNoticeDismissed || {} ),
|
||||
[ productId ]: 'yes',
|
||||
},
|
||||
} );
|
||||
} }
|
||||
noticeActions={ [
|
||||
{
|
||||
label: __( 'Set prices', 'woocommerce' ),
|
||||
onClick: onSetPrices,
|
||||
className: 'is-destructive',
|
||||
},
|
||||
] }
|
||||
onVariationTableChange={ ( type, update ) => {
|
||||
if (
|
||||
type === 'delete' ||
|
||||
( type === 'update' &&
|
||||
update &&
|
||||
update.find(
|
||||
( variation ) =>
|
||||
variation.regular_price ||
|
||||
variation.sale_price
|
||||
) )
|
||||
) {
|
||||
invalidateResolution(
|
||||
'getProductVariationsTotalCount',
|
||||
[ totalCountWithoutPriceRequestParams ]
|
||||
);
|
||||
}
|
||||
} }
|
||||
/>
|
||||
{ context?.isInSelectedTab && <VariableProductTour /> }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -403,7 +403,9 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
}
|
||||
value={
|
||||
attribute ===
|
||||
null
|
||||
null ||
|
||||
attribute ===
|
||||
undefined
|
||||
? []
|
||||
: attribute.terms
|
||||
}
|
||||
|
@ -506,7 +508,9 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
|||
label={ addAccessibleLabel }
|
||||
disabled={
|
||||
values.attributes.length === 1 &&
|
||||
values.attributes[ 0 ] === null
|
||||
( values.attributes[ 0 ] === null ||
|
||||
values.attributes[ 0 ] ===
|
||||
undefined )
|
||||
}
|
||||
onClick={ () =>
|
||||
onAddingAttributes( values )
|
||||
|
|
|
@ -89,7 +89,9 @@ export const AttributeListItem: React.FC< AttributeListItemProps > = ( {
|
|||
position="top center"
|
||||
text={ NOT_VISIBLE_TEXT }
|
||||
>
|
||||
<HiddenWithHelpIcon />
|
||||
<div>
|
||||
<HiddenWithHelpIcon />
|
||||
</div>
|
||||
</Tooltip>
|
||||
) }
|
||||
{ typeof onEditClick === 'function' && (
|
||||
|
|
|
@ -26,7 +26,7 @@ export function usePublish( {
|
|||
onPublishSuccess?( product: Product ): void;
|
||||
onPublishError?( error: WPError ): void;
|
||||
} ): Button.ButtonProps {
|
||||
const { isValidating, validate } = useValidations();
|
||||
const { isValidating, validate } = useValidations< Product >();
|
||||
|
||||
const [ productId ] = useEntityProp< number >(
|
||||
'postType',
|
||||
|
@ -61,7 +61,9 @@ export function usePublish( {
|
|||
}
|
||||
|
||||
try {
|
||||
await validate();
|
||||
await validate( {
|
||||
status: 'publish',
|
||||
} );
|
||||
|
||||
// The publish button click not only change the status of the product
|
||||
// but also save all the pending changes. So even if the status is
|
||||
|
@ -93,6 +95,12 @@ export function usePublish( {
|
|||
? 'product_publish_error'
|
||||
: 'product_create_error',
|
||||
} as WPError;
|
||||
if ( ( error as Record< string, string > ).variations ) {
|
||||
wpError.code = 'variable_product_no_variation_prices';
|
||||
wpError.message = (
|
||||
error as Record< string, string >
|
||||
).variations;
|
||||
}
|
||||
}
|
||||
onPublishError( wpError );
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export function useSaveDraft( {
|
|||
[ productId ]
|
||||
);
|
||||
|
||||
const { isValidating, validate } = useValidations();
|
||||
const { isValidating, validate } = useValidations< Product >();
|
||||
|
||||
const ariaDisabled =
|
||||
disabled ||
|
||||
|
@ -76,7 +76,7 @@ export function useSaveDraft( {
|
|||
}
|
||||
|
||||
try {
|
||||
await validate();
|
||||
await validate( { status: 'draft' } );
|
||||
|
||||
await editEntityRecord( 'postType', 'product', productId, {
|
||||
status: 'draft',
|
||||
|
|
|
@ -15,6 +15,21 @@ $table-row-height: calc($grid-unit * 9);
|
|||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
&__notice {
|
||||
border-left: 0px;
|
||||
margin: 0 0 $gap-large 0;
|
||||
&.is-error {
|
||||
background-color: #fcf0f1;
|
||||
}
|
||||
|
||||
.components-notice__actions {
|
||||
margin-top: $gap-small;
|
||||
.components-button:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
height: $table-row-height * 5;
|
||||
overflow: auto;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { __, sprintf } from '@wordpress/i18n';
|
|||
import {
|
||||
Button,
|
||||
CheckboxControl,
|
||||
Notice,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
} from '@wordpress/components';
|
||||
|
@ -27,6 +28,8 @@ import {
|
|||
createElement,
|
||||
useRef,
|
||||
useMemo,
|
||||
Fragment,
|
||||
forwardRef,
|
||||
} from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import classnames from 'classnames';
|
||||
|
@ -53,7 +56,38 @@ import HiddenWithHelpIcon from '../../icons/hidden-with-help-icon';
|
|||
|
||||
const NOT_VISIBLE_TEXT = __( 'Not visible to customers', 'woocommerce' );
|
||||
|
||||
export function VariationsTable() {
|
||||
type VariationsTableProps = {
|
||||
noticeText?: string;
|
||||
noticeStatus?: 'error' | 'warning' | 'success' | 'info';
|
||||
onNoticeDismiss?: () => void;
|
||||
noticeActions?: {
|
||||
label: string;
|
||||
onClick: (
|
||||
handleUpdateAll: ( update: Partial< ProductVariation >[] ) => void,
|
||||
handleDeleteAll: ( update: Partial< ProductVariation >[] ) => void
|
||||
) => void;
|
||||
className?: string;
|
||||
variant?: string;
|
||||
}[];
|
||||
onVariationTableChange?: (
|
||||
type: 'update' | 'delete',
|
||||
updates?: Partial< ProductVariation >[]
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const VariationsTable = forwardRef<
|
||||
HTMLDivElement,
|
||||
VariationsTableProps
|
||||
>( function Table(
|
||||
{
|
||||
noticeText,
|
||||
noticeActions = [],
|
||||
noticeStatus = 'error',
|
||||
onNoticeDismiss = () => {},
|
||||
onVariationTableChange = () => {},
|
||||
}: VariationsTableProps,
|
||||
ref
|
||||
) {
|
||||
const [ currentPage, setCurrentPage ] = useState( 1 );
|
||||
const lastVariations = useRef< ProductVariation[] | null >( null );
|
||||
const [ perPage, setPerPage ] = useState(
|
||||
|
@ -90,22 +124,9 @@ export function VariationsTable() {
|
|||
} ),
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const context = useContext( CurrencyContext );
|
||||
const { formatAmount } = context;
|
||||
const { totalCount } = useSelect(
|
||||
( select ) => {
|
||||
const { getProductVariationsTotalCount } = select(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
totalCount: getProductVariationsTotalCount< number >(
|
||||
totalCountRequestParams
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
const { isLoading, latestVariations, isGeneratingVariations } = useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
|
@ -127,6 +148,21 @@ export function VariationsTable() {
|
|||
[ currentPage, perPage, productId ]
|
||||
);
|
||||
|
||||
const { totalCount } = useSelect(
|
||||
( select ) => {
|
||||
const { getProductVariationsTotalCount } = select(
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
return {
|
||||
totalCount: getProductVariationsTotalCount< number >(
|
||||
totalCountRequestParams
|
||||
),
|
||||
};
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const paginationProps = usePagination( {
|
||||
totalCount,
|
||||
defaultPerPage: DEFAULT_VARIATION_PER_PAGE_OPTION,
|
||||
|
@ -179,13 +215,17 @@ export function VariationsTable() {
|
|||
recordEvent( 'product_variations_delete', {
|
||||
source: TRACKS_SOURCE,
|
||||
} );
|
||||
invalidateResolution( 'getProductVariations', [
|
||||
requestParams,
|
||||
] );
|
||||
} )
|
||||
.finally( () =>
|
||||
.finally( () => {
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: false,
|
||||
} ) )
|
||||
);
|
||||
} ) );
|
||||
onVariationTableChange( 'delete' );
|
||||
} );
|
||||
}
|
||||
|
||||
function handleVariationChange(
|
||||
|
@ -212,12 +252,13 @@ export function VariationsTable() {
|
|||
__( 'Failed to save variation.', 'woocommerce' )
|
||||
);
|
||||
} )
|
||||
.finally( () =>
|
||||
.finally( () => {
|
||||
setIsUpdating( ( prevState ) => ( {
|
||||
...prevState,
|
||||
[ variationId ]: false,
|
||||
} ) )
|
||||
);
|
||||
} ) );
|
||||
onVariationTableChange( 'update', [ variation ] );
|
||||
} );
|
||||
}
|
||||
|
||||
function handleUpdateAll( update: Partial< ProductVariation >[] ) {
|
||||
|
@ -238,6 +279,7 @@ export function VariationsTable() {
|
|||
response.update.length
|
||||
)
|
||||
);
|
||||
onVariationTableChange( 'update', update );
|
||||
} )
|
||||
.catch( () => {
|
||||
createErrorNotice(
|
||||
|
@ -266,6 +308,7 @@ export function VariationsTable() {
|
|||
response.delete.length
|
||||
)
|
||||
);
|
||||
onVariationTableChange( 'delete' );
|
||||
} )
|
||||
.catch( () => {
|
||||
createErrorNotice(
|
||||
|
@ -275,7 +318,7 @@ export function VariationsTable() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-product-variations">
|
||||
<div className="woocommerce-product-variations" ref={ ref }>
|
||||
{ ( isLoading || isGeneratingVariations ) && (
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
<Spinner />
|
||||
|
@ -286,6 +329,21 @@ export function VariationsTable() {
|
|||
) }
|
||||
</div>
|
||||
) }
|
||||
{ noticeText && (
|
||||
<Notice
|
||||
status={ noticeStatus }
|
||||
className="woocommerce-product-variations__notice"
|
||||
onRemove={ onNoticeDismiss }
|
||||
actions={ noticeActions.map( ( action ) => ( {
|
||||
...action,
|
||||
onClick: () => {
|
||||
action?.onClick( handleUpdateAll, handleDeleteAll );
|
||||
},
|
||||
} ) ) }
|
||||
>
|
||||
{ noticeText }
|
||||
</Notice>
|
||||
) }
|
||||
<div className="woocommerce-product-variations__header">
|
||||
<div className="woocommerce-product-variations__selection">
|
||||
<CheckboxControl
|
||||
|
@ -401,18 +459,25 @@ export function VariationsTable() {
|
|||
}
|
||||
) }
|
||||
>
|
||||
<span
|
||||
className={ classnames(
|
||||
'woocommerce-product-variations__status-dot',
|
||||
getProductStockStatusClass( variation )
|
||||
) }
|
||||
>
|
||||
●
|
||||
</span>
|
||||
{ getProductStockStatus( variation ) }
|
||||
{ variation.regular_price && (
|
||||
<>
|
||||
<span
|
||||
className={ classnames(
|
||||
'woocommerce-product-variations__status-dot',
|
||||
getProductStockStatusClass(
|
||||
variation
|
||||
)
|
||||
) }
|
||||
>
|
||||
●
|
||||
</span>
|
||||
{ getProductStockStatus( variation ) }
|
||||
</>
|
||||
) }
|
||||
</div>
|
||||
<div className="woocommerce-product-variations__actions">
|
||||
{ variation.status === 'private' && (
|
||||
{ ( variation.status === 'private' ||
|
||||
! variation.regular_price ) && (
|
||||
<Tooltip
|
||||
// @ts-expect-error className is missing in TS, should remove this when it is included.
|
||||
className="woocommerce-attribute-list-item__actions-tooltip"
|
||||
|
@ -459,4 +524,4 @@ export function VariationsTable() {
|
|||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
export type ValidatorResponse = Promise< ValidationError >;
|
||||
|
||||
export type Validator< T > = ( initialValue?: T ) => ValidatorResponse;
|
||||
export type Validator< T > = (
|
||||
initialValue?: T,
|
||||
newData?: Partial< T >
|
||||
) => ValidatorResponse;
|
||||
|
||||
export type ValidationContextProps< T > = {
|
||||
errors: ValidationErrors;
|
||||
|
@ -9,7 +12,7 @@ export type ValidationContextProps< T > = {
|
|||
validator: Validator< T >
|
||||
): React.Ref< HTMLElement >;
|
||||
validateField( name: string ): ValidatorResponse;
|
||||
validateAll(): Promise< ValidationErrors >;
|
||||
validateAll( newData?: Partial< T > ): Promise< ValidationErrors >;
|
||||
};
|
||||
|
||||
export type ValidationProviderProps< T > = {
|
||||
|
@ -19,9 +22,9 @@ export type ValidationProviderProps< T > = {
|
|||
export type ValidationError = string | undefined;
|
||||
export type ValidationErrors = Record< string, ValidationError >;
|
||||
|
||||
export type ValidatorRegistration = {
|
||||
export type ValidatorRegistration< T > = {
|
||||
name: string;
|
||||
ref: React.Ref< HTMLElement >;
|
||||
error?: ValidationError;
|
||||
validate(): ValidatorResponse;
|
||||
validate( newData?: Partial< T > ): ValidatorResponse;
|
||||
};
|
||||
|
|
|
@ -13,17 +13,17 @@ function isInvalid( errors: ValidationErrors ) {
|
|||
return Object.values( errors ).some( Boolean );
|
||||
}
|
||||
|
||||
export function useValidations() {
|
||||
export function useValidations< T = unknown >() {
|
||||
const context = useContext( ValidationContext );
|
||||
const [ isValidating, setIsValidating ] = useState( false );
|
||||
|
||||
return {
|
||||
isValidating,
|
||||
async validate() {
|
||||
async validate( newData?: Partial< T > ) {
|
||||
setIsValidating( true );
|
||||
return new Promise< void >( ( resolve, reject ) => {
|
||||
context
|
||||
.validateAll()
|
||||
.validateAll( newData )
|
||||
.then( ( errors ) => {
|
||||
if ( isInvalid( errors ) ) {
|
||||
reject( errors );
|
||||
|
|
|
@ -38,11 +38,14 @@ export function ValidationProvider< T >( {
|
|||
};
|
||||
}
|
||||
|
||||
async function validateField( validatorId: string ): ValidatorResponse {
|
||||
async function validateField(
|
||||
validatorId: string,
|
||||
newData?: Partial< T >
|
||||
): ValidatorResponse {
|
||||
const validators = validatorsRef.current;
|
||||
if ( validatorId in validators ) {
|
||||
const validator = validators[ validatorId ];
|
||||
const result = validator( initialValue );
|
||||
const result = validator( initialValue, newData );
|
||||
|
||||
return result.then( ( error ) => {
|
||||
setErrors( ( currentErrors ) => ( {
|
||||
|
@ -56,12 +59,17 @@ export function ValidationProvider< T >( {
|
|||
return Promise.resolve( undefined );
|
||||
}
|
||||
|
||||
async function validateAll(): Promise< ValidationErrors > {
|
||||
async function validateAll(
|
||||
newData: Partial< T >
|
||||
): Promise< ValidationErrors > {
|
||||
const newErrors: ValidationErrors = {};
|
||||
const validators = validatorsRef.current;
|
||||
|
||||
for ( const validatorId in validators ) {
|
||||
newErrors[ validatorId ] = await validateField( validatorId );
|
||||
newErrors[ validatorId ] = await validateField(
|
||||
validatorId,
|
||||
newData
|
||||
);
|
||||
}
|
||||
|
||||
setErrors( newErrors );
|
||||
|
|
|
@ -33,5 +33,8 @@ export function useConfirmUnsavedProductChanges() {
|
|||
[ productId ]
|
||||
);
|
||||
|
||||
useConfirmUnsavedChanges( hasEdits || isSaving, preventLeavingProductForm );
|
||||
useConfirmUnsavedChanges(
|
||||
hasEdits || isSaving,
|
||||
preventLeavingProductForm( productId )
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { resolveSelect, useDispatch } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useCallback, useState } from '@wordpress/element';
|
||||
import { getNewPath, getPath, navigateTo } from '@woocommerce/navigation';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
Product,
|
||||
ProductDefaultAttribute,
|
||||
} from '@woocommerce/data';
|
||||
|
||||
|
@ -34,6 +36,13 @@ export function useProductVariationsHelper() {
|
|||
) => {
|
||||
setIsGenerating( true );
|
||||
|
||||
const lastStatus = (
|
||||
( await resolveSelect( 'core' ).getEditedEntityRecord(
|
||||
'postType',
|
||||
'product',
|
||||
productId
|
||||
) ) as Product
|
||||
).status;
|
||||
const hasVariableAttribute = attributes.some(
|
||||
( attr ) => attr.variation
|
||||
);
|
||||
|
@ -64,6 +73,13 @@ export function useProductVariationsHelper() {
|
|||
} )
|
||||
.finally( () => {
|
||||
setIsGenerating( false );
|
||||
if (
|
||||
lastStatus === 'auto-draft' &&
|
||||
getPath().endsWith( 'add-product' )
|
||||
) {
|
||||
const url = getNewPath( {}, `/product/${ productId }` );
|
||||
navigateTo( { url } );
|
||||
}
|
||||
} );
|
||||
},
|
||||
[]
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
export type WPErrorCode =
|
||||
| 'variable_product_no_variation_prices'
|
||||
| 'product_invalid_sku'
|
||||
| 'product_create_error'
|
||||
| 'product_publish_error'
|
||||
|
@ -19,6 +20,8 @@ export type WPError = {
|
|||
|
||||
export function getProductErrorMessage( error: WPError ) {
|
||||
switch ( error.code ) {
|
||||
case 'variable_product_no_variation_prices':
|
||||
return error.message;
|
||||
case 'product_invalid_sku':
|
||||
return __( 'Invalid or duplicated SKU.', 'woocommerce' );
|
||||
case 'product_create_error':
|
||||
|
|
|
@ -6,10 +6,19 @@ import { Location } from 'react-router-dom';
|
|||
/**
|
||||
* Allow switching between tabs without prompting for unsaved changes.
|
||||
*/
|
||||
export const preventLeavingProductForm = ( toUrl: URL, fromUrl: Location ) => {
|
||||
const toParams = new URLSearchParams( toUrl.search );
|
||||
const fromParams = new URLSearchParams( fromUrl.search );
|
||||
toParams.delete( 'tab' );
|
||||
fromParams.delete( 'tab' );
|
||||
return toParams.toString() !== fromParams.toString();
|
||||
};
|
||||
export const preventLeavingProductForm =
|
||||
( productId?: number ) => ( toUrl: URL, fromUrl: Location ) => {
|
||||
const toParams = new URLSearchParams( toUrl.search );
|
||||
const fromParams = new URLSearchParams( fromUrl.search );
|
||||
toParams.delete( 'tab' );
|
||||
fromParams.delete( 'tab' );
|
||||
// Prevent dialog from happening if moving from add new to edit page of same product.
|
||||
if (
|
||||
productId !== undefined &&
|
||||
fromParams.get( 'path' ) === '/add-product' &&
|
||||
toParams.get( 'path' ) === '/product/' + productId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return toParams.toString() !== fromParams.toString();
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ describe( 'preventLeavingProductForm', () => {
|
|||
const fromUrl = {
|
||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=general',
|
||||
} as Location;
|
||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
||||
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||
expect( shouldPrevent ).toBe( true );
|
||||
} );
|
||||
|
||||
|
@ -27,7 +27,7 @@ describe( 'preventLeavingProductForm', () => {
|
|||
const fromUrl = {
|
||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=general',
|
||||
} as Location;
|
||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
||||
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||
expect( shouldPrevent ).toBe( true );
|
||||
} );
|
||||
|
||||
|
@ -38,7 +38,35 @@ describe( 'preventLeavingProductForm', () => {
|
|||
const fromUrl = {
|
||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=shipping',
|
||||
} as Location;
|
||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
||||
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||
expect( shouldPrevent ).toBe( true );
|
||||
} );
|
||||
|
||||
it( 'should allow leaving when moving from the add-product to the edit page with same product id', () => {
|
||||
const toUrl = new URL(
|
||||
'http://mysite.com/admin.php?page=wc-admin&path=/product/123&tab=general'
|
||||
);
|
||||
const fromUrl = {
|
||||
search: 'admin.php?page=wc-admin&path=/add-product',
|
||||
} as Location;
|
||||
const shouldPrevent = preventLeavingProductForm( 123 )(
|
||||
toUrl,
|
||||
fromUrl
|
||||
);
|
||||
expect( shouldPrevent ).toBe( false );
|
||||
} );
|
||||
|
||||
it( 'should not allow leaving when moving from the add-product to the edit page with different product id', () => {
|
||||
const toUrl = new URL(
|
||||
'http://mysite.com/admin.php?page=wc-admin&path=/product/123&tab=general'
|
||||
);
|
||||
const fromUrl = {
|
||||
search: 'admin.php?page=wc-admin&path=/add-product',
|
||||
} as Location;
|
||||
const shouldPrevent = preventLeavingProductForm( 333 )(
|
||||
toUrl,
|
||||
fromUrl
|
||||
);
|
||||
expect( shouldPrevent ).toBe( true );
|
||||
} );
|
||||
|
||||
|
@ -49,7 +77,7 @@ describe( 'preventLeavingProductForm', () => {
|
|||
const fromUrl = {
|
||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=shipping&other_param=b',
|
||||
} as Location;
|
||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
||||
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||
expect( shouldPrevent ).toBe( true );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1492,11 +1492,15 @@ export const CoreProfilerController = ( {
|
|||
hasJetpackSelected: ( context ) => {
|
||||
return (
|
||||
context.pluginsSelected.find(
|
||||
( plugin ) => plugin === 'jetpack'
|
||||
( plugin ) =>
|
||||
plugin === 'jetpack' ||
|
||||
plugin === 'jetpack-boost'
|
||||
) !== undefined ||
|
||||
context.pluginsAvailable.find(
|
||||
( plugin: Extension ) =>
|
||||
plugin.key === 'jetpack' && plugin.is_activated
|
||||
( plugin.key === 'jetpack' ||
|
||||
plugin.key === 'jetpack-boost' ) &&
|
||||
plugin.is_activated
|
||||
) !== undefined
|
||||
);
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose';
|
||||
import { useMemo, useContext } from '@wordpress/element';
|
||||
import { useContext } from '@wordpress/element';
|
||||
import { Disabled } from '@wordpress/components';
|
||||
import {
|
||||
__unstableEditorStyles as EditorStyles,
|
||||
|
@ -52,7 +52,6 @@ export type ScaledBlockPreviewProps = {
|
|||
function ScaledBlockPreview( {
|
||||
viewportWidth,
|
||||
containerWidth,
|
||||
minHeight,
|
||||
settings,
|
||||
additionalStyles,
|
||||
onClickNavigationItem,
|
||||
|
@ -70,28 +69,14 @@ function ScaledBlockPreview( {
|
|||
viewportWidth = containerWidth;
|
||||
}
|
||||
|
||||
// @ts-ignore No types for this exist yet.
|
||||
const [ contentResizeListener, { height: contentHeight } ] =
|
||||
useResizeObserver();
|
||||
|
||||
// Avoid scrollbars for pattern previews.
|
||||
const editorStyles = useMemo( () => {
|
||||
return [
|
||||
{
|
||||
css: 'body{height:auto;overflow:hidden;border:none;padding:0;}',
|
||||
__unstableType: 'presets',
|
||||
},
|
||||
...settings.styles,
|
||||
];
|
||||
}, [ settings.styles ] );
|
||||
|
||||
// Initialize on render instead of module top level, to avoid circular dependency issues.
|
||||
MemoizedBlockList = MemoizedBlockList || pure( BlockList );
|
||||
const scale = containerWidth / viewportWidth;
|
||||
|
||||
return (
|
||||
<DisabledProvider value={ true }>
|
||||
<Iframe
|
||||
aria-hidden
|
||||
tabIndex={ -1 }
|
||||
contentRef={ useRefEffect( ( bodyElement: HTMLBodyElement ) => {
|
||||
const {
|
||||
ownerDocument: { documentElement },
|
||||
|
@ -225,18 +210,8 @@ function ScaledBlockPreview( {
|
|||
} );
|
||||
};
|
||||
}, [] ) }
|
||||
aria-hidden
|
||||
tabIndex={ -1 }
|
||||
style={ {
|
||||
width: viewportWidth,
|
||||
height: contentHeight,
|
||||
minHeight:
|
||||
scale !== 0 && scale < 1 && minHeight
|
||||
? minHeight / scale
|
||||
: minHeight,
|
||||
} }
|
||||
>
|
||||
<EditorStyles styles={ editorStyles } />
|
||||
<EditorStyles styles={ settings.styles } />
|
||||
<style>
|
||||
{ `
|
||||
.block-editor-block-list__block::before,
|
||||
|
@ -267,7 +242,6 @@ function ScaledBlockPreview( {
|
|||
${ additionalStyles }
|
||||
` }
|
||||
</style>
|
||||
{ contentResizeListener }
|
||||
<MemoizedBlockList renderAppender={ false } />
|
||||
{ /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ }
|
||||
{ externalFontFamilies.length === 2 && (
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
// @ts-ignore No types for this exist yet.
|
||||
import { useEntityRecords } from '@wordpress/core-data';
|
||||
// @ts-ignore No types for this exist yet.
|
||||
|
@ -46,7 +45,7 @@ export const BlockEditor = ( {} ) => {
|
|||
: 'topDown';
|
||||
|
||||
const previewOpacity = useScrollOpacity(
|
||||
'.interface-navigable-region.interface-interface-skeleton__content',
|
||||
'.woocommerce-customize-store__block-editor iframe',
|
||||
scrollDirection
|
||||
);
|
||||
|
||||
|
@ -92,59 +91,6 @@ export const BlockEditor = ( {} ) => {
|
|||
[ history, urlParams, pages ]
|
||||
);
|
||||
|
||||
if ( urlParams.path === '/customize-store/assembler-hub/homepage' ) {
|
||||
// When assembling the homepage preview, we need to render the blocks in a different way than the rest of the pages.
|
||||
// Because we want to show a action bar when hovering over a pattern. This is not needed for the rest of the pages and will cause an issue with logo editing.
|
||||
return (
|
||||
<div className="woocommerce-customize-store__block-editor">
|
||||
{ blocks.map( ( block, index ) => {
|
||||
// Add padding to the top and bottom of the block preview.
|
||||
let additionalStyles = '';
|
||||
let hasActionBar = false;
|
||||
switch ( true ) {
|
||||
case index === 0:
|
||||
// header
|
||||
additionalStyles = `
|
||||
.editor-styles-wrapper{ padding-top: var(--wp--style--root--padding-top) };'
|
||||
`;
|
||||
break;
|
||||
|
||||
case index === blocks.length - 1:
|
||||
// footer
|
||||
additionalStyles = `
|
||||
.editor-styles-wrapper{ padding-bottom: var(--wp--style--root--padding-bottom) };
|
||||
`;
|
||||
break;
|
||||
default:
|
||||
hasActionBar = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={ block.clientId }
|
||||
className={ classNames(
|
||||
'woocommerce-block-preview-container',
|
||||
{
|
||||
'has-action-menu': hasActionBar,
|
||||
}
|
||||
) }
|
||||
>
|
||||
<BlockPreview
|
||||
blocks={ block }
|
||||
settings={ settings }
|
||||
additionalStyles={ additionalStyles }
|
||||
onClickNavigationItem={ onClickNavigationItem }
|
||||
// Use sub registry because we have multiple previews
|
||||
useSubRegistry={ true }
|
||||
previewOpacity={ previewOpacity }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="woocommerce-customize-store__block-editor">
|
||||
<div className={ 'woocommerce-block-preview-container' }>
|
||||
|
|
|
@ -24,14 +24,14 @@ export const useEditorScroll = ( {
|
|||
}
|
||||
|
||||
const previewContainer =
|
||||
document.querySelector< HTMLDivElement >( editorSelector );
|
||||
document.querySelector< HTMLIFrameElement >( editorSelector );
|
||||
if ( previewContainer ) {
|
||||
previewContainer?.scrollTo(
|
||||
previewContainer.contentWindow?.scrollTo(
|
||||
0,
|
||||
scrollDirection === 'bottom'
|
||||
? previewContainer?.scrollHeight
|
||||
? previewContainer.contentDocument?.body.scrollHeight || 0
|
||||
: 0
|
||||
);
|
||||
}
|
||||
}, [ isEditorLoading, scrollDirection ] );
|
||||
}, [ isEditorLoading, editorSelector, scrollDirection ] );
|
||||
};
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
/* eslint-disable @woocommerce/dependency-group */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
import { parse } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { usePatterns, Pattern, PatternWithBlocks } from './use-patterns';
|
||||
|
||||
// TODO: It might be better to create an API endpoint to get the templates.
|
||||
const LARGE_BUSINESS_TEMPLATES = {
|
||||
template1: [
|
||||
'a8c/cover-image-with-left-aligned-call-to-action',
|
||||
'woocommerce-blocks/featured-products-5-item-grid',
|
||||
'woocommerce-blocks/featured-products-fresh-and-tasty',
|
||||
'woocommerce-blocks/featured-category-triple',
|
||||
'a8c/3-column-testimonials',
|
||||
'a8c/quotes-2',
|
||||
'woocommerce-blocks/social-follow-us-in-social-media',
|
||||
],
|
||||
template2: [
|
||||
'woocommerce-blocks/hero-product-split',
|
||||
'woocommerce-blocks/featured-products-fresh-and-tasty',
|
||||
'woocommerce-blocks/featured-category-triple',
|
||||
'woocommerce-blocks/featured-products-fresh-and-tasty',
|
||||
'a8c/three-columns-with-images-and-text',
|
||||
'woocommerce-blocks/testimonials-3-columns',
|
||||
'a8c/subscription',
|
||||
],
|
||||
template3: [
|
||||
'a8c/call-to-action-7',
|
||||
'a8c/3-column-testimonials',
|
||||
'woocommerce-blocks/featured-products-fresh-and-tasty',
|
||||
'woocommerce-blocks/featured-category-cover-image',
|
||||
'woocommerce-blocks/featured-products-5-item-grid',
|
||||
'woocommerce-blocks/featured-products-5-item-grid',
|
||||
'woocommerce-blocks/social-follow-us-in-social-media',
|
||||
],
|
||||
};
|
||||
|
||||
const SMALL_MEDIUM_BUSINESS_TEMPLATES = {
|
||||
template1: [
|
||||
'woocommerce-blocks/featured-products-fresh-and-tasty',
|
||||
'woocommerce-blocks/testimonials-single',
|
||||
'woocommerce-blocks/hero-product-3-split',
|
||||
'a8c/contact-8',
|
||||
],
|
||||
template2: [
|
||||
'a8c/about-me-4',
|
||||
'a8c/product-feature-with-buy-button',
|
||||
'woocommerce-blocks/featured-products-fresh-and-tasty',
|
||||
'a8c/subscription',
|
||||
'woocommerce-blocks/testimonials-3-columns',
|
||||
'a8c/contact-with-map-on-the-left',
|
||||
],
|
||||
template3: [
|
||||
'a8c/heading-and-video',
|
||||
'a8c/3-column-testimonials',
|
||||
'woocommerce-blocks/product-hero',
|
||||
'a8c/quotes-2',
|
||||
'a8c/product-feature-with-buy-button',
|
||||
'a8c/simple-two-column-layout',
|
||||
'woocommerce-blocks/social-follow-us-in-social-media',
|
||||
],
|
||||
};
|
||||
|
||||
const getTemplatePatterns = (
|
||||
template: string[],
|
||||
patternsByName: Record< string, Pattern >
|
||||
) =>
|
||||
template
|
||||
.map( ( patternName: string ) => {
|
||||
const pattern = patternsByName[ patternName ];
|
||||
if ( pattern && pattern.content ) {
|
||||
return {
|
||||
...pattern,
|
||||
// @ts-ignore - Passing options is valid, but not in the type.
|
||||
blocks: parse( pattern.content, {
|
||||
__unstableSkipMigrationLogs: true,
|
||||
} ),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} )
|
||||
.filter( ( pattern ) => pattern !== null ) as PatternWithBlocks[];
|
||||
|
||||
export const useHomeTemplates = () => {
|
||||
// TODO: Get businessType from option
|
||||
const businessType = 'SMB' as string;
|
||||
const { blockPatterns, isLoading } = usePatterns();
|
||||
|
||||
const patternsByName = useMemo( () => {
|
||||
return blockPatterns.reduce(
|
||||
( acc: Record< string, Pattern >, pattern: Pattern ) => {
|
||||
acc[ pattern.name ] = pattern;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}, [ blockPatterns ] );
|
||||
|
||||
const homeTemplates = useMemo( () => {
|
||||
if ( isLoading ) return {};
|
||||
const recommendedTemplates =
|
||||
businessType === 'SMB'
|
||||
? SMALL_MEDIUM_BUSINESS_TEMPLATES
|
||||
: LARGE_BUSINESS_TEMPLATES;
|
||||
|
||||
return Object.entries( recommendedTemplates ).reduce(
|
||||
(
|
||||
acc: Record< string, PatternWithBlocks[] >,
|
||||
[ templateName, template ]
|
||||
) => {
|
||||
if ( templateName in recommendedTemplates ) {
|
||||
acc[ templateName ] = getTemplatePatterns(
|
||||
template,
|
||||
patternsByName
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}, [ isLoading, patternsByName ] );
|
||||
|
||||
return {
|
||||
homeTemplates,
|
||||
isLoading,
|
||||
};
|
||||
};
|
|
@ -9,7 +9,7 @@ import { store as coreStore } from '@wordpress/core-data';
|
|||
import { useMemo } from '@wordpress/element';
|
||||
import { BlockInstance, parse } from '@wordpress/blocks';
|
||||
|
||||
type Pattern = {
|
||||
export type Pattern = {
|
||||
blockTypes: string[];
|
||||
categories: string[];
|
||||
content: string;
|
||||
|
@ -18,15 +18,17 @@ type Pattern = {
|
|||
title: string;
|
||||
};
|
||||
|
||||
type PatternWithBlocks = Pattern & {
|
||||
export type PatternWithBlocks = Pattern & {
|
||||
blocks: BlockInstance[];
|
||||
};
|
||||
|
||||
export const usePatternsByCategory = ( category: string ) => {
|
||||
export const usePatterns = () => {
|
||||
const { blockPatterns, isLoading } = useSelect(
|
||||
( select ) => ( {
|
||||
// @ts-ignore - This is valid.
|
||||
blockPatterns: select( coreStore ).getBlockPatterns(),
|
||||
blockPatterns: select(
|
||||
coreStore
|
||||
// @ts-ignore - This is valid.
|
||||
).getBlockPatterns() as Pattern[],
|
||||
isLoading:
|
||||
// @ts-ignore - This is valid.
|
||||
! select( coreStore ).hasFinishedResolution(
|
||||
|
@ -36,6 +38,15 @@ export const usePatternsByCategory = ( category: string ) => {
|
|||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
blockPatterns,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePatternsByCategory = ( category: string ) => {
|
||||
const { blockPatterns, isLoading } = usePatterns();
|
||||
|
||||
const patternsByCategory: PatternWithBlocks[] = useMemo( () => {
|
||||
return ( blockPatterns || [] )
|
||||
.filter( ( pattern: Pattern ) =>
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
/* eslint-disable @woocommerce/dependency-group */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
// @ts-ignore No types for this exist yet.
|
||||
import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks';
|
||||
|
||||
type ScrollDirection = 'topDown' | 'bottomUp';
|
||||
|
||||
|
@ -11,35 +15,50 @@ export const useScrollOpacity = (
|
|||
sensitivity = 0.2
|
||||
) => {
|
||||
const [ opacity, setOpacity ] = useState( 0.05 );
|
||||
const isEditorLoading = useIsSiteEditorLoading();
|
||||
|
||||
useEffect( () => {
|
||||
const targetElement = document.querySelector( selector );
|
||||
let targetElement: Document | Element | null =
|
||||
document.querySelector( selector );
|
||||
|
||||
const isIFrame = targetElement?.tagName === 'IFRAME';
|
||||
if ( isIFrame ) {
|
||||
targetElement = ( targetElement as HTMLIFrameElement )
|
||||
.contentDocument;
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
if ( targetElement ) {
|
||||
const maxScrollHeight =
|
||||
targetElement.scrollHeight - targetElement.clientHeight;
|
||||
const currentScrollPosition = targetElement.scrollTop;
|
||||
const maxEffectScroll = maxScrollHeight * sensitivity;
|
||||
|
||||
let calculatedOpacity;
|
||||
if ( direction === 'bottomUp' ) {
|
||||
calculatedOpacity =
|
||||
1 - currentScrollPosition / maxEffectScroll;
|
||||
} else {
|
||||
calculatedOpacity = currentScrollPosition / maxEffectScroll;
|
||||
}
|
||||
|
||||
calculatedOpacity = 0.1 + 0.9 * calculatedOpacity;
|
||||
|
||||
// Clamp opacity between 0.1 and 1
|
||||
calculatedOpacity = Math.max(
|
||||
0.1,
|
||||
Math.min( calculatedOpacity, 1 )
|
||||
);
|
||||
|
||||
setOpacity( calculatedOpacity );
|
||||
if ( ! targetElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentElement = isIFrame
|
||||
? ( targetElement as Document ).documentElement
|
||||
: ( targetElement as Element );
|
||||
|
||||
const maxScrollHeight =
|
||||
contentElement.scrollHeight - contentElement.clientHeight;
|
||||
const currentScrollPosition = contentElement.scrollTop;
|
||||
const maxEffectScroll = maxScrollHeight * sensitivity;
|
||||
|
||||
let calculatedOpacity;
|
||||
if ( direction === 'bottomUp' ) {
|
||||
calculatedOpacity =
|
||||
maxScrollHeight / maxEffectScroll -
|
||||
currentScrollPosition / maxEffectScroll;
|
||||
} else {
|
||||
calculatedOpacity = currentScrollPosition / maxEffectScroll;
|
||||
}
|
||||
|
||||
calculatedOpacity = 0.1 + 0.9 * calculatedOpacity;
|
||||
|
||||
// Clamp opacity between 0.1 and 1
|
||||
calculatedOpacity = Math.max(
|
||||
0.1,
|
||||
Math.min( calculatedOpacity, 1 )
|
||||
);
|
||||
|
||||
setOpacity( calculatedOpacity );
|
||||
};
|
||||
|
||||
if ( targetElement ) {
|
||||
|
@ -51,7 +70,7 @@ export const useScrollOpacity = (
|
|||
targetElement.removeEventListener( 'scroll', handleScroll );
|
||||
}
|
||||
};
|
||||
}, [ selector, direction, sensitivity ] );
|
||||
}, [ selector, direction, sensitivity, isEditorLoading ] );
|
||||
|
||||
return opacity;
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { TourKit, TourKitTypes } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
export * from './use-onboarding-tour';
|
||||
|
||||
type OnboardingTourProps = {
|
||||
|
@ -83,8 +84,15 @@ export const OnboardingTour = ( {
|
|||
],
|
||||
closeHandler: ( _steps, _currentStepIndex, source ) => {
|
||||
if ( source === 'done-btn' ) {
|
||||
// Click on "Take a tour" button
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_tour_start'
|
||||
);
|
||||
setShowWelcomeTour( false );
|
||||
} else {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_tour_skip'
|
||||
);
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
|
@ -196,7 +204,19 @@ export const OnboardingTour = ( {
|
|||
},
|
||||
},
|
||||
],
|
||||
closeHandler: onClose,
|
||||
closeHandler: ( _steps, _currentStepIndex, source ) => {
|
||||
if ( source === 'done-btn' ) {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_tour_complete'
|
||||
);
|
||||
} else {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_tour_close'
|
||||
);
|
||||
}
|
||||
|
||||
onClose();
|
||||
},
|
||||
} }
|
||||
></TourKit>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { OnboardingTour } from '../index';
|
||||
|
||||
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
|
||||
|
||||
describe( 'OnboardingTour', () => {
|
||||
let props: {
|
||||
onClose: jest.Mock;
|
||||
setShowWelcomeTour: jest.Mock;
|
||||
showWelcomeTour: boolean;
|
||||
};
|
||||
|
||||
beforeEach( () => {
|
||||
props = {
|
||||
onClose: jest.fn(),
|
||||
setShowWelcomeTour: jest.fn(),
|
||||
showWelcomeTour: true,
|
||||
};
|
||||
} );
|
||||
|
||||
it( 'should render welcome tour', () => {
|
||||
render( <OnboardingTour { ...props } /> );
|
||||
|
||||
expect(
|
||||
screen.getByText( /Welcome to your AI-generated store!/i )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render step 1', () => {
|
||||
render( <OnboardingTour { ...props } showWelcomeTour={ false } /> );
|
||||
|
||||
expect(
|
||||
screen.getByText( /View your changes in real time/i )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should record an event when clicking on "Take a tour" button', () => {
|
||||
render( <OnboardingTour { ...props } /> );
|
||||
|
||||
screen
|
||||
.getByRole( 'button', {
|
||||
name: /Take a tour/i,
|
||||
} )
|
||||
.click();
|
||||
|
||||
expect( recordEvent ).toHaveBeenCalledWith(
|
||||
'customize_your_store_assembler_hub_tour_start'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should record an event when clicking on "Skip" button', () => {
|
||||
render( <OnboardingTour { ...props } /> );
|
||||
|
||||
screen
|
||||
.getByRole( 'button', {
|
||||
name: /Skip/i,
|
||||
} )
|
||||
.click();
|
||||
|
||||
expect( recordEvent ).toHaveBeenCalledWith(
|
||||
'customize_your_store_assembler_hub_tour_skip'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should record an event when clicking on "Skip" button', () => {
|
||||
render( <OnboardingTour { ...props } showWelcomeTour={ false } /> );
|
||||
|
||||
screen
|
||||
.getByRole( 'button', {
|
||||
name: 'Close Tour',
|
||||
} )
|
||||
.click();
|
||||
|
||||
expect( recordEvent ).toHaveBeenCalledWith(
|
||||
'customize_your_store_assembler_hub_tour_close'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should record an event when complete the tour', () => {
|
||||
render( <OnboardingTour { ...props } showWelcomeTour={ false } /> );
|
||||
|
||||
screen
|
||||
.getByRole( 'button', {
|
||||
name: 'Next',
|
||||
} )
|
||||
.click();
|
||||
|
||||
screen
|
||||
.getByRole( 'button', {
|
||||
name: 'Done',
|
||||
} )
|
||||
.click();
|
||||
|
||||
expect( recordEvent ).toHaveBeenCalledWith(
|
||||
'customize_your_store_assembler_hub_tour_complete'
|
||||
);
|
||||
} );
|
||||
} );
|
|
@ -23,7 +23,7 @@ import { SidebarNavigationScreenTypography } from './sidebar-navigation-screen-t
|
|||
import { SidebarNavigationScreenHeader } from './sidebar-navigation-screen-header';
|
||||
import { SidebarNavigationScreenHomepage } from './sidebar-navigation-screen-homepage';
|
||||
import { SidebarNavigationScreenFooter } from './sidebar-navigation-screen-footer';
|
||||
import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages';
|
||||
// import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages';
|
||||
import { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo';
|
||||
|
||||
import { SaveHub } from './save-hub';
|
||||
|
@ -124,9 +124,10 @@ function SidebarScreens() {
|
|||
<NavigatorScreen path="/customize-store/assembler-hub/footer">
|
||||
<SidebarNavigationScreenFooter />
|
||||
</NavigatorScreen>
|
||||
<NavigatorScreen path="/customize-store/assembler-hub/pages">
|
||||
{ /* TODO: Implement pages sidebar in Phrase 2 */ }
|
||||
{ /* <NavigatorScreen path="/customize-store/assembler-hub/pages">
|
||||
<SidebarNavigationScreenPages />
|
||||
</NavigatorScreen>
|
||||
</NavigatorScreen> */ }
|
||||
<NavigatorScreen path="/customize-store/assembler-hub/logo">
|
||||
<SidebarNavigationScreenLogo />
|
||||
</NavigatorScreen>
|
||||
|
|
|
@ -22,6 +22,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
|||
import { store as noticesStore } from '@wordpress/notices';
|
||||
// @ts-ignore No types for this exist yet.
|
||||
import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -142,6 +143,13 @@ export const SaveHub = () => {
|
|||
}, [ urlParams.path ] );
|
||||
|
||||
const save = async () => {
|
||||
const source = `${ urlParams.path.replace(
|
||||
'/customize-store/assembler-hub/',
|
||||
''
|
||||
) }`;
|
||||
recordEvent( 'customize_your_store_assembler_hub_save_click', {
|
||||
source,
|
||||
} );
|
||||
removeNotice( saveNoticeId );
|
||||
|
||||
try {
|
||||
|
@ -185,6 +193,10 @@ export const SaveHub = () => {
|
|||
<Button
|
||||
variant="primary"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_done_click'
|
||||
);
|
||||
|
||||
setIsResolving( true );
|
||||
sendEvent( 'FINISH_CUSTOMIZATION' );
|
||||
} }
|
||||
|
|
|
@ -15,6 +15,7 @@ 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';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -70,14 +71,38 @@ export const SidebarNavigationScreenColorPalette = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'color-palette',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
StyleLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_style_link_click',
|
||||
{
|
||||
source: 'color-palette',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Spinner } from '@wordpress/components';
|
||||
// @ts-expect-error Missing type in core-data.
|
||||
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
||||
|
@ -34,8 +35,7 @@ const SUPPORTED_FOOTER_PATTERNS = [
|
|||
|
||||
export const SidebarNavigationScreenFooter = () => {
|
||||
useEditorScroll( {
|
||||
editorSelector:
|
||||
'.interface-navigable-region.interface-interface-skeleton__content',
|
||||
editorSelector: '.woocommerce-customize-store__block-editor iframe',
|
||||
scrollDirection: 'bottom',
|
||||
} );
|
||||
|
||||
|
@ -86,8 +86,20 @@ export const SidebarNavigationScreenFooter = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'footer',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Spinner } from '@wordpress/components';
|
||||
// @ts-ignore No types for this exist yet.
|
||||
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
||||
|
@ -35,8 +36,7 @@ const SUPPORTED_HEADER_PATTERNS = [
|
|||
|
||||
export const SidebarNavigationScreenHeader = () => {
|
||||
useEditorScroll( {
|
||||
editorSelector:
|
||||
'.interface-navigable-region.interface-interface-skeleton__content',
|
||||
editorSelector: '.woocommerce-customize-store__block-editor iframe',
|
||||
scrollDirection: 'top',
|
||||
} );
|
||||
|
||||
|
@ -85,8 +85,20 @@ export const SidebarNavigationScreenHeader = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'header',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
|
|
@ -1,16 +1,61 @@
|
|||
/* eslint-disable @woocommerce/dependency-group */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import {
|
||||
createInterpolateElement,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { Spinner } from '@wordpress/components';
|
||||
|
||||
// @ts-expect-error Missing type in core-data.
|
||||
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
|
||||
import { ADMIN_URL } from '~/utils/admin-settings';
|
||||
import { useEditorBlocks } from '../hooks/use-editor-blocks';
|
||||
import { useHomeTemplates } from '../hooks/use-home-templates';
|
||||
import { BlockInstance } from '@wordpress/blocks';
|
||||
|
||||
export const SidebarNavigationScreenHomepage = () => {
|
||||
const { isLoading, homeTemplates } = useHomeTemplates();
|
||||
|
||||
const [ blocks, , onChange ] = useEditorBlocks();
|
||||
const onClickPattern = useCallback(
|
||||
( _pattern, selectedBlocks ) => {
|
||||
onChange(
|
||||
[ blocks[ 0 ], ...selectedBlocks, blocks[ blocks.length - 1 ] ],
|
||||
{ selection: {} }
|
||||
);
|
||||
},
|
||||
[ blocks, onChange ]
|
||||
);
|
||||
|
||||
const homePatterns = useMemo( () => {
|
||||
return Object.entries( homeTemplates ).map(
|
||||
( [ templateName, patterns ] ) => {
|
||||
return {
|
||||
name: templateName,
|
||||
blocks: patterns.reduce(
|
||||
( acc: BlockInstance[], pattern ) => [
|
||||
...acc,
|
||||
...pattern.blocks,
|
||||
],
|
||||
[]
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [ homeTemplates ] );
|
||||
|
||||
return (
|
||||
<SidebarNavigationScreen
|
||||
title={ __( 'Change your homepage', 'woocommerce' ) }
|
||||
|
@ -22,16 +67,45 @@ export const SidebarNavigationScreenHomepage = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'homepage',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
}
|
||||
) }
|
||||
content={
|
||||
<>
|
||||
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
|
||||
</>
|
||||
<div className="woocommerce-customize-store__sidebar-homepage-content">
|
||||
<div className="edit-site-sidebar-navigation-screen-patterns__group-homepage">
|
||||
{ isLoading ? (
|
||||
<span className="components-placeholder__preview">
|
||||
<Spinner />
|
||||
</span>
|
||||
) : (
|
||||
<BlockPatternList
|
||||
shownPatterns={ homePatterns }
|
||||
blockPatterns={ homePatterns }
|
||||
onClickPattern={ onClickPattern }
|
||||
label={ 'Hompeage' }
|
||||
orientation="vertical"
|
||||
category={ 'homepage' }
|
||||
isDraggable={ false }
|
||||
showTitlesAsTooltip={ false }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -20,11 +20,11 @@ import {
|
|||
header,
|
||||
home,
|
||||
footer,
|
||||
pages,
|
||||
} from '@wordpress/icons';
|
||||
// @ts-ignore No types for this exist yet.
|
||||
import SidebarNavigationItem from '@wordpress/edit-site/build-module/components/sidebar-navigation-item';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -45,8 +45,20 @@ export const SidebarNavigationScreenMain = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'main',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
@ -64,6 +76,14 @@ export const SidebarNavigationScreenMain = () => {
|
|||
path="/customize-store/assembler-hub/logo"
|
||||
withChevron
|
||||
icon={ siteLogo }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||
{
|
||||
item: 'logo',
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __( 'Add your logo', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
|
@ -72,6 +92,14 @@ export const SidebarNavigationScreenMain = () => {
|
|||
path="/customize-store/assembler-hub/color-palette"
|
||||
withChevron
|
||||
icon={ color }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||
{
|
||||
item: 'color-palette',
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __( 'Change the color palette', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
|
@ -80,6 +108,14 @@ export const SidebarNavigationScreenMain = () => {
|
|||
path="/customize-store/assembler-hub/typography"
|
||||
withChevron
|
||||
icon={ typography }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||
{
|
||||
item: 'typography',
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __( 'Change fonts', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
|
@ -95,6 +131,14 @@ export const SidebarNavigationScreenMain = () => {
|
|||
path="/customize-store/assembler-hub/header"
|
||||
withChevron
|
||||
icon={ header }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||
{
|
||||
item: 'header',
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __( 'Change your header', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
|
@ -103,6 +147,14 @@ export const SidebarNavigationScreenMain = () => {
|
|||
path="/customize-store/assembler-hub/homepage"
|
||||
withChevron
|
||||
icon={ home }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||
{
|
||||
item: 'home',
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __( 'Change your homepage', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
|
@ -111,17 +163,26 @@ export const SidebarNavigationScreenMain = () => {
|
|||
path="/customize-store/assembler-hub/footer"
|
||||
withChevron
|
||||
icon={ footer }
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||
{
|
||||
item: 'footer',
|
||||
}
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __( 'Change your footer', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
<NavigatorButton
|
||||
{ /* TODO: Turn on this in Phrase 2 */ }
|
||||
{ /* <NavigatorButton
|
||||
as={ SidebarNavigationItem }
|
||||
path="/customize-store/assembler-hub/pages"
|
||||
withChevron
|
||||
icon={ pages }
|
||||
>
|
||||
{ __( 'Add and edit other pages', 'woocommerce' ) }
|
||||
</NavigatorButton>
|
||||
</NavigatorButton> */ }
|
||||
</ItemGroup>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -23,8 +24,20 @@ export const SidebarNavigationScreenPages = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'pages',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
PageLink: (
|
||||
|
|
|
@ -14,6 +14,7 @@ import { noop } from 'lodash';
|
|||
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';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -42,14 +43,38 @@ export const SidebarNavigationScreenTypography = () => {
|
|||
{
|
||||
EditorLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_editor_link_click',
|
||||
{
|
||||
source: 'typography',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
StyleLink: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'customize_your_store_assembler_hub_style_link_click',
|
||||
{
|
||||
source: 'typography',
|
||||
}
|
||||
);
|
||||
window.open(
|
||||
`${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit`,
|
||||
'_blank'
|
||||
);
|
||||
return false;
|
||||
} }
|
||||
href=""
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
padding-top: 80px;
|
||||
padding-bottom: 0;
|
||||
gap: 0;
|
||||
width: 348px;
|
||||
}
|
||||
|
||||
.edit-site-sidebar-navigation-screen__title-icon,
|
||||
|
@ -243,9 +244,13 @@
|
|||
|
||||
.woocommerce-customize-store__sidebar-logo-container {
|
||||
margin: 12px 0 32px;
|
||||
width: 324px;
|
||||
padding: 32px;
|
||||
cursor: pointer;
|
||||
width: 324px;
|
||||
|
||||
.woocommerce-customize-store_custom-logo {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-customize-store__sidebar-logo-content {
|
||||
|
@ -411,6 +416,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.edit-site-sidebar-navigation-screen__content .block-editor-block-patterns-list {
|
||||
width: 324px;
|
||||
}
|
||||
|
||||
.woocommerce-customize-store__sidebar-homepage-content {
|
||||
.block-editor-block-preview__content {
|
||||
background-color: #fff;
|
||||
max-height: 280px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Preview Canvas */
|
||||
.edit-site-layout__canvas {
|
||||
|
@ -452,10 +467,18 @@
|
|||
.woocommerce-customize-store__block-editor,
|
||||
.edit-site-layout:not(.is-full-canvas) .edit-site-layout__canvas > div .interface-interface-skeleton__content {
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.interface-interface-skeleton__content {
|
||||
@include custom-scrollbars-on-hover(transparent, $gray-600);
|
||||
.woocommerce-customize-store__block-editor,
|
||||
.woocommerce-block-preview-container,
|
||||
.auto-block-preview__container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-site-resizable-frame__inner-content {
|
||||
|
@ -526,3 +549,42 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-site-sidebar-navigation-screen-patterns__group-homepage {
|
||||
.woocommerce-collapsible-content:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.woocommerce-collapsible-content {
|
||||
padding: 16px 0 16px 0;
|
||||
border-bottom: 1px solid #ededed;
|
||||
button {
|
||||
width: 100%;
|
||||
color: #1e1e1e;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.15px;
|
||||
}
|
||||
svg {
|
||||
margin-left: auto;
|
||||
fill: #1e1e1e;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,10 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
ColorPalette,
|
||||
designWithAiStateMachineContext,
|
||||
designWithAiStateMachineEvents,
|
||||
LookAndToneCompletionResponse,
|
||||
} from './types';
|
||||
import { aiWizardClosedBeforeCompletionEvent } from './events';
|
||||
import {
|
||||
|
@ -18,7 +20,6 @@ import {
|
|||
lookAndFeelCompleteEvent,
|
||||
toneOfVoiceCompleteEvent,
|
||||
} from './pages';
|
||||
import { LookAndToneCompletionResponse } from './services';
|
||||
|
||||
const assignBusinessInfoDescription = assign<
|
||||
designWithAiStateMachineContext,
|
||||
|
@ -72,14 +73,28 @@ const assignLookAndTone = assign<
|
|||
},
|
||||
} );
|
||||
|
||||
const assignDefaultColorPalette = assign<
|
||||
designWithAiStateMachineContext,
|
||||
designWithAiStateMachineEvents
|
||||
>( {
|
||||
aiSuggestions: ( context, event: unknown ) => {
|
||||
return {
|
||||
...context.aiSuggestions,
|
||||
defaultColorPalette: (
|
||||
event as {
|
||||
data: {
|
||||
response: ColorPalette;
|
||||
};
|
||||
}
|
||||
).data.response,
|
||||
};
|
||||
},
|
||||
} );
|
||||
|
||||
const logAIAPIRequestError = () => {
|
||||
// log AI API request error
|
||||
// eslint-disable-next-line no-console
|
||||
console.log( 'API Request error' );
|
||||
recordEvent(
|
||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
||||
{ error_type: 'http_network_error' }
|
||||
);
|
||||
};
|
||||
|
||||
const updateQueryStep = (
|
||||
|
@ -142,6 +157,7 @@ export const actions = {
|
|||
assignLookAndFeel,
|
||||
assignToneOfVoice,
|
||||
assignLookAndTone,
|
||||
assignDefaultColorPalette,
|
||||
logAIAPIRequestError,
|
||||
updateQueryStep,
|
||||
recordTracksStepViewed,
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
} from './pages';
|
||||
import { customizeStoreStateMachineEvents } from '..';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
export type events = { type: 'THEME_SUGGESTED' };
|
||||
export type DesignWithAiComponent =
|
||||
| typeof BusinessInfoDescription
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { defaultColorPalette } from '.';
|
||||
|
||||
describe( 'colorPairing.responseValidation', () => {
|
||||
it( 'should validate a correct color palette', () => {
|
||||
const validPalette = {
|
||||
name: 'Ancient Bronze',
|
||||
primary: '#11163d',
|
||||
secondary: '#8C8369',
|
||||
foreground: '#11163d',
|
||||
background: '#ffffff',
|
||||
};
|
||||
|
||||
const parsedResult =
|
||||
defaultColorPalette.responseValidation( validPalette );
|
||||
expect( parsedResult ).toEqual( validPalette );
|
||||
} );
|
||||
|
||||
it( 'should fail for an incorrect name', () => {
|
||||
const invalidPalette = {
|
||||
name: 'Invalid Name',
|
||||
primary: '#11163d',
|
||||
secondary: '#8C8369',
|
||||
foreground: '#11163d',
|
||||
background: '#ffffff',
|
||||
};
|
||||
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||
.toThrowErrorMatchingInlineSnapshot( `
|
||||
"[
|
||||
{
|
||||
\\"code\\": \\"custom\\",
|
||||
\\"message\\": \\"Color palette not part of allowed list\\",
|
||||
\\"path\\": [
|
||||
\\"name\\"
|
||||
]
|
||||
}
|
||||
]"
|
||||
` );
|
||||
} );
|
||||
|
||||
it( 'should fail for an invalid primary color', () => {
|
||||
const invalidPalette = {
|
||||
name: 'Ancient Bronze',
|
||||
primary: 'invalidColor',
|
||||
secondary: '#8C8369',
|
||||
foreground: '#11163d',
|
||||
background: '#ffffff',
|
||||
};
|
||||
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||
.toThrowErrorMatchingInlineSnapshot( `
|
||||
"[
|
||||
{
|
||||
\\"validation\\": \\"regex\\",
|
||||
\\"code\\": \\"invalid_string\\",
|
||||
\\"message\\": \\"Invalid primary color\\",
|
||||
\\"path\\": [
|
||||
\\"primary\\"
|
||||
]
|
||||
}
|
||||
]"
|
||||
` );
|
||||
} );
|
||||
|
||||
it( 'should fail for an invalid secondary color', () => {
|
||||
const invalidPalette = {
|
||||
name: 'Ancient Bronze',
|
||||
primary: '#11163d',
|
||||
secondary: 'invalidColor',
|
||||
foreground: '#11163d',
|
||||
background: '#ffffff',
|
||||
};
|
||||
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||
.toThrowErrorMatchingInlineSnapshot( `
|
||||
"[
|
||||
{
|
||||
\\"validation\\": \\"regex\\",
|
||||
\\"code\\": \\"invalid_string\\",
|
||||
\\"message\\": \\"Invalid secondary color\\",
|
||||
\\"path\\": [
|
||||
\\"secondary\\"
|
||||
]
|
||||
}
|
||||
]"
|
||||
` );
|
||||
} );
|
||||
|
||||
it( 'should fail for an invalid foreground color', () => {
|
||||
const invalidPalette = {
|
||||
name: 'Ancient Bronze',
|
||||
primary: '#11163d',
|
||||
secondary: '11163d',
|
||||
foreground: '#invalid_color',
|
||||
background: '#ffffff',
|
||||
};
|
||||
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||
.toThrowErrorMatchingInlineSnapshot( `
|
||||
"[
|
||||
{
|
||||
\\"validation\\": \\"regex\\",
|
||||
\\"code\\": \\"invalid_string\\",
|
||||
\\"message\\": \\"Invalid secondary color\\",
|
||||
\\"path\\": [
|
||||
\\"secondary\\"
|
||||
]
|
||||
},
|
||||
{
|
||||
\\"validation\\": \\"regex\\",
|
||||
\\"code\\": \\"invalid_string\\",
|
||||
\\"message\\": \\"Invalid foreground color\\",
|
||||
\\"path\\": [
|
||||
\\"foreground\\"
|
||||
]
|
||||
}
|
||||
]"
|
||||
` );
|
||||
} );
|
||||
|
||||
it( 'should fail for an invalid background color', () => {
|
||||
const invalidPalette = {
|
||||
name: 'Ancient Bronze',
|
||||
primary: '#11163d',
|
||||
secondary: '#11163d',
|
||||
foreground: '#11163d',
|
||||
background: '#fffff',
|
||||
};
|
||||
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||
.toThrowErrorMatchingInlineSnapshot( `
|
||||
"[
|
||||
{
|
||||
\\"validation\\": \\"regex\\",
|
||||
\\"code\\": \\"invalid_string\\",
|
||||
\\"message\\": \\"Invalid background color\\",
|
||||
\\"path\\": [
|
||||
\\"background\\"
|
||||
]
|
||||
}
|
||||
]"
|
||||
` );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,240 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ColorPalette } from '../types';
|
||||
|
||||
const colorChoices: ColorPalette[] = [
|
||||
{
|
||||
name: 'Ancient Bronze',
|
||||
primary: '#11163d',
|
||||
secondary: '#8C8369',
|
||||
foreground: '#11163d',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Crimson Tide',
|
||||
primary: '#A02040',
|
||||
secondary: '#234B57',
|
||||
foreground: '#871C37',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Purple Twilight',
|
||||
primary: '#301834',
|
||||
secondary: '#6a5eb7',
|
||||
foreground: '#090909',
|
||||
background: '#fefbff',
|
||||
},
|
||||
{
|
||||
name: 'Midnight Citrus',
|
||||
primary: '#1B1736',
|
||||
secondary: '#7E76A3',
|
||||
foreground: '#1B1736',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Lemon Myrtle',
|
||||
primary: '#3E7172',
|
||||
secondary: '#FC9B00',
|
||||
foreground: '#325C5D',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Green Thumb',
|
||||
primary: '#164A41',
|
||||
secondary: '#4B7B4D',
|
||||
foreground: '#164A41',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Golden Haze',
|
||||
primary: '#232224',
|
||||
secondary: '#EBB54F',
|
||||
foreground: '#515151',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Golden Indigo',
|
||||
primary: '#4866C0',
|
||||
secondary: '#C09F50',
|
||||
foreground: '#405AA7',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Arctic Dawn',
|
||||
primary: '#243156',
|
||||
secondary: '#DE5853',
|
||||
foreground: '#243156',
|
||||
background: '#ffffff',
|
||||
},
|
||||
{
|
||||
name: 'Jungle Sunrise',
|
||||
primary: '#1a4435',
|
||||
secondary: '#ed774e',
|
||||
foreground: '#0a271d',
|
||||
background: '#fefbec',
|
||||
},
|
||||
{
|
||||
name: 'Berry Grove',
|
||||
primary: '#1F351A',
|
||||
secondary: '#DE76DE',
|
||||
foreground: '#1f351a',
|
||||
background: '#fdfaf1',
|
||||
},
|
||||
{
|
||||
name: 'Fuchsia',
|
||||
primary: '#b7127f',
|
||||
secondary: '#18020C',
|
||||
foreground: '#b7127f',
|
||||
background: '#f7edf6',
|
||||
},
|
||||
{
|
||||
name: 'Raspberry Chocolate',
|
||||
primary: '#42332e',
|
||||
secondary: '#d64d68',
|
||||
foreground: '#241d1a',
|
||||
background: '#eeeae6',
|
||||
},
|
||||
{
|
||||
name: 'Canary',
|
||||
primary: '#0F0F05',
|
||||
secondary: '#353535',
|
||||
foreground: '#0F0F05',
|
||||
background: '#FCFF9B',
|
||||
},
|
||||
{
|
||||
name: 'Gumtree Sunset',
|
||||
primary: '#476C77',
|
||||
secondary: '#EFB071',
|
||||
foreground: '#476C77',
|
||||
background: '#edf4f4',
|
||||
},
|
||||
{
|
||||
name: 'Ice',
|
||||
primary: '#12123F',
|
||||
secondary: '#3473FE',
|
||||
foreground: '#12123F',
|
||||
background: '#F1F4FA',
|
||||
},
|
||||
{
|
||||
name: 'Cinder',
|
||||
primary: '#c14420',
|
||||
secondary: '#2F2D2D',
|
||||
foreground: '#863119',
|
||||
background: '#f1f2f2',
|
||||
},
|
||||
{
|
||||
name: 'Blue Lagoon',
|
||||
primary: '#004DE5',
|
||||
secondary: '#0496FF',
|
||||
foreground: '#0036A3',
|
||||
background: '#FEFDF8',
|
||||
},
|
||||
{
|
||||
name: 'Sandalwood Oasis',
|
||||
primary: '#F0EBE3',
|
||||
secondary: '#DF9785',
|
||||
foreground: '#ffffff',
|
||||
background: '#2a2a16',
|
||||
},
|
||||
{
|
||||
name: 'Rustic Rosewood',
|
||||
primary: '#F4F4F2',
|
||||
secondary: '#EE797C',
|
||||
foreground: '#ffffff',
|
||||
background: '#1A1A1A',
|
||||
},
|
||||
{
|
||||
name: 'Cinnamon Latte',
|
||||
primary: '#D9CAB3',
|
||||
secondary: '#BC8034',
|
||||
foreground: '#FFFFFF',
|
||||
background: '#3C3F4D',
|
||||
},
|
||||
{
|
||||
name: 'Lilac Nightshade',
|
||||
primary: '#f5d6ff',
|
||||
secondary: '#C48DDA',
|
||||
foreground: '#ffffff',
|
||||
background: '#000000',
|
||||
},
|
||||
{
|
||||
name: 'Lightning',
|
||||
primary: '#ebffd2',
|
||||
secondary: '#fefefe',
|
||||
foreground: '#ebffd2',
|
||||
background: '#0e1fb5',
|
||||
},
|
||||
{
|
||||
name: 'Aquamarine Night',
|
||||
primary: '#deffef',
|
||||
secondary: '#56fbb9',
|
||||
foreground: '#ffffff',
|
||||
background: '#091C48',
|
||||
},
|
||||
{
|
||||
name: 'Charcoal',
|
||||
primary: '#dbdbdb',
|
||||
secondary: '#efefef',
|
||||
foreground: '#dbdbdb',
|
||||
background: '#1e1e1e',
|
||||
},
|
||||
{
|
||||
name: 'Evergreen Twilight',
|
||||
primary: '#ffffff',
|
||||
secondary: '#8EE978',
|
||||
foreground: '#ffffff',
|
||||
background: '#181818',
|
||||
},
|
||||
{
|
||||
name: 'Slate',
|
||||
primary: '#FFFFFF',
|
||||
secondary: '#FFDF6D',
|
||||
foreground: '#EFF2F9',
|
||||
background: '#13161E',
|
||||
},
|
||||
];
|
||||
const allowedNames: string[] = colorChoices.map( ( palette ) => palette.name );
|
||||
const hexColorRegex = /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
|
||||
|
||||
export const colorPaletteValidator = z.object( {
|
||||
name: z.string().refine( ( name ) => allowedNames.includes( name ), {
|
||||
message: 'Color palette not part of allowed list',
|
||||
} ),
|
||||
primary: z
|
||||
.string()
|
||||
.regex( hexColorRegex, { message: 'Invalid primary color' } ),
|
||||
secondary: z
|
||||
.string()
|
||||
.regex( hexColorRegex, { message: 'Invalid secondary color' } ),
|
||||
foreground: z
|
||||
.string()
|
||||
.regex( hexColorRegex, { message: 'Invalid foreground color' } ),
|
||||
background: z
|
||||
.string()
|
||||
.regex( hexColorRegex, { message: 'Invalid background color' } ),
|
||||
} );
|
||||
|
||||
export const defaultColorPalette = {
|
||||
queryId: 'default_color_palette',
|
||||
|
||||
// make sure version is updated every time the prompt is changed
|
||||
version: '2023-09-18',
|
||||
prompt: ( businessDescription: string, look: string, tone: string ) => {
|
||||
return `
|
||||
You are a WordPress theme expert. Analyse the following store description, merchant's chosen look and tone, and determine the most appropriate color scheme.
|
||||
Respond only with one color scheme and only its JSON.
|
||||
|
||||
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||
Business description: ${ businessDescription }
|
||||
|
||||
Colors to choose from:
|
||||
${ JSON.stringify( colorChoices ) }
|
||||
`;
|
||||
},
|
||||
responseValidation: colorPaletteValidator.parse,
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './colorChoices';
|
||||
export * from './lookAndTone';
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { LookAndToneCompletionResponse } from '../types';
|
||||
import { lookAndTone } from '.';
|
||||
|
||||
describe( 'parseLookAndToneCompletionResponse', () => {
|
||||
it( 'should return a valid object when given valid JSON', () => {
|
||||
const validObj = JSON.parse(
|
||||
'{"look": "Contemporary", "tone": "Neutral"}'
|
||||
);
|
||||
const result = lookAndTone.responseValidation( validObj );
|
||||
const expected: LookAndToneCompletionResponse = {
|
||||
look: 'Contemporary',
|
||||
tone: 'Neutral',
|
||||
};
|
||||
expect( result ).toEqual( expected );
|
||||
} );
|
||||
|
||||
it( 'should throw an error and record an event for valid JSON but invalid values', () => {
|
||||
const invalidValuesObj = {
|
||||
completion: '{"look": "Invalid", "tone": "Invalid"}',
|
||||
};
|
||||
expect( () =>
|
||||
lookAndTone.responseValidation( invalidValuesObj )
|
||||
).toThrow( 'Invalid values in Look and Tone completion response' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
Look,
|
||||
Tone,
|
||||
VALID_LOOKS,
|
||||
VALID_TONES,
|
||||
LookAndToneCompletionResponse,
|
||||
} from '../types';
|
||||
|
||||
export const isLookAndToneCompletionResponse = (
|
||||
obj: unknown
|
||||
): obj is LookAndToneCompletionResponse => {
|
||||
return (
|
||||
obj !== undefined &&
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'look' in obj &&
|
||||
VALID_LOOKS.includes( obj.look as Look ) &&
|
||||
'tone' in obj &&
|
||||
VALID_TONES.includes( obj.tone as Tone )
|
||||
);
|
||||
};
|
||||
|
||||
export const lookAndTone = {
|
||||
queryId: 'look_and_tone',
|
||||
// make sure version is updated every time the prompt is changed
|
||||
version: '2023-09-18',
|
||||
prompt: ( businessInfoDescription: string ) => {
|
||||
return [
|
||||
'You are a WordPress theme expert.',
|
||||
'Analyze the following store description and determine the look and tone of the theme.',
|
||||
`For look, you can choose between ${ VALID_LOOKS.join( ',' ) }.`,
|
||||
`For tone of the description, you can choose between ${ VALID_TONES.join(
|
||||
','
|
||||
) }.`,
|
||||
'Your response should be in json with look and tone values.',
|
||||
'\n',
|
||||
businessInfoDescription,
|
||||
].join( '\n' );
|
||||
},
|
||||
responseValidation: ( response: unknown ) => {
|
||||
if ( isLookAndToneCompletionResponse( response ) ) {
|
||||
return response;
|
||||
}
|
||||
throw new Error(
|
||||
'Invalid values in Look and Tone completion response'
|
||||
);
|
||||
},
|
||||
};
|
|
@ -4,103 +4,13 @@
|
|||
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Sender } from 'xstate';
|
||||
import { Sender, assign, createMachine } from 'xstate';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
Look,
|
||||
Tone,
|
||||
VALID_LOOKS,
|
||||
VALID_TONES,
|
||||
designWithAiStateMachineContext,
|
||||
} from './types';
|
||||
|
||||
export interface LookAndToneCompletionResponse {
|
||||
look: Look;
|
||||
tone: Tone;
|
||||
}
|
||||
|
||||
interface MaybeLookAndToneCompletionResponse {
|
||||
completion: string;
|
||||
}
|
||||
|
||||
export const isLookAndToneCompletionResponse = (
|
||||
obj: unknown
|
||||
): obj is LookAndToneCompletionResponse => {
|
||||
return (
|
||||
obj !== undefined &&
|
||||
obj !== null &&
|
||||
typeof obj === 'object' &&
|
||||
'look' in obj &&
|
||||
VALID_LOOKS.includes( obj.look as Look ) &&
|
||||
'tone' in obj &&
|
||||
VALID_TONES.includes( obj.tone as Tone )
|
||||
);
|
||||
};
|
||||
|
||||
export const parseLookAndToneCompletionResponse = (
|
||||
obj: MaybeLookAndToneCompletionResponse
|
||||
): LookAndToneCompletionResponse => {
|
||||
try {
|
||||
const o = JSON.parse( obj.completion );
|
||||
if ( isLookAndToneCompletionResponse( o ) ) {
|
||||
return o;
|
||||
}
|
||||
} catch {
|
||||
recordEvent(
|
||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
||||
{ error_type: 'json_parse_error', response: JSON.stringify( obj ) }
|
||||
);
|
||||
}
|
||||
recordEvent(
|
||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
||||
{
|
||||
error_type: 'valid_json_invalid_values',
|
||||
response: JSON.stringify( obj ),
|
||||
}
|
||||
);
|
||||
throw new Error( 'Could not parse Look and Tone completion response.' );
|
||||
};
|
||||
|
||||
export const getLookAndTone = async (
|
||||
context: designWithAiStateMachineContext
|
||||
) => {
|
||||
const prompt = [
|
||||
'You are a WordPress theme expert.',
|
||||
'Analyze the following store description and determine the look and tone of the theme.',
|
||||
`For look, you can choose between ${ VALID_LOOKS.join( ',' ) }.`,
|
||||
`For tone of the description, you can choose between ${ VALID_TONES.join(
|
||||
','
|
||||
) }.`,
|
||||
'Your response should be in json with look and tone values.',
|
||||
'\n',
|
||||
context.businessInfoDescription.descriptionText,
|
||||
];
|
||||
|
||||
const { token } = await requestJetpackToken();
|
||||
|
||||
const url = new URL(
|
||||
'https://public-api.wordpress.com/wpcom/v2/text-completion'
|
||||
);
|
||||
|
||||
url.searchParams.append( 'feature', 'woo_cys' );
|
||||
|
||||
const data: {
|
||||
completion: string;
|
||||
} = await apiFetch( {
|
||||
url: url.toString(),
|
||||
method: 'POST',
|
||||
data: {
|
||||
token,
|
||||
prompt: prompt.join( '\n' ),
|
||||
_fields: 'completion',
|
||||
},
|
||||
} );
|
||||
|
||||
return parseLookAndToneCompletionResponse( data );
|
||||
};
|
||||
import { designWithAiStateMachineContext } from './types';
|
||||
import { lookAndTone } from './prompts';
|
||||
|
||||
const browserPopstateHandler =
|
||||
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
|
||||
|
@ -113,7 +23,177 @@ const browserPopstateHandler =
|
|||
};
|
||||
};
|
||||
|
||||
export const getCompletion = async < ValidResponseObject >( {
|
||||
queryId,
|
||||
prompt,
|
||||
version,
|
||||
responseValidation,
|
||||
retryCount,
|
||||
}: {
|
||||
queryId: string;
|
||||
prompt: string;
|
||||
version: string;
|
||||
responseValidation: ( arg0: string ) => ValidResponseObject;
|
||||
retryCount: number;
|
||||
} ) => {
|
||||
const { token } = await requestJetpackToken();
|
||||
let data: {
|
||||
completion: string;
|
||||
};
|
||||
let parsedCompletionJson;
|
||||
try {
|
||||
const url = new URL(
|
||||
'https://public-api.wordpress.com/wpcom/v2/text-completion'
|
||||
);
|
||||
|
||||
url.searchParams.append( 'feature', 'woo_cys' );
|
||||
|
||||
data = await apiFetch( {
|
||||
url: url.toString(),
|
||||
method: 'POST',
|
||||
data: {
|
||||
token,
|
||||
prompt,
|
||||
_fields: 'completion',
|
||||
},
|
||||
} );
|
||||
} catch ( error ) {
|
||||
recordEvent( 'customize_your_store_ai_completion_api_error', {
|
||||
query_id: queryId,
|
||||
version,
|
||||
retry_count: retryCount,
|
||||
error_type: 'api_request_error',
|
||||
} );
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
parsedCompletionJson = JSON.parse( data.completion );
|
||||
} catch {
|
||||
recordEvent( 'customize_your_store_ai_completion_response_error', {
|
||||
query_id: queryId,
|
||||
version,
|
||||
retry_count: retryCount,
|
||||
error_type: 'json_parse_error',
|
||||
response: data.completion,
|
||||
} );
|
||||
throw new Error(
|
||||
`Error validating Jetpack AI text completions response for ${ queryId }`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const validatedResponse = responseValidation( parsedCompletionJson );
|
||||
recordEvent( 'customize_your_store_ai_completion_success', {
|
||||
query_id: queryId,
|
||||
version,
|
||||
retry_count: retryCount,
|
||||
} );
|
||||
return validatedResponse;
|
||||
} catch ( error ) {
|
||||
recordEvent( 'customize_your_store_ai_completion_response_error', {
|
||||
query_id: queryId,
|
||||
version,
|
||||
retry_count: retryCount,
|
||||
error_type: 'valid_json_invalid_values',
|
||||
response: data.completion,
|
||||
} );
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLookAndTone = async (
|
||||
context: designWithAiStateMachineContext
|
||||
) => {
|
||||
return getCompletion( {
|
||||
...lookAndTone,
|
||||
prompt: lookAndTone.prompt(
|
||||
context.businessInfoDescription.descriptionText
|
||||
),
|
||||
retryCount: 0,
|
||||
} );
|
||||
};
|
||||
|
||||
export const queryAiEndpoint = createMachine(
|
||||
{
|
||||
id: 'query-ai-endpoint',
|
||||
predictableActionArguments: true,
|
||||
initial: 'init',
|
||||
context: {
|
||||
// these values are all overwritten by incoming parameters
|
||||
prompt: '',
|
||||
queryId: '',
|
||||
version: '',
|
||||
responseValidation: () => true,
|
||||
retryCount: 0,
|
||||
validatedResponse: {} as unknown,
|
||||
},
|
||||
states: {
|
||||
init: {
|
||||
always: 'querying',
|
||||
entry: [ 'setRetryCount' ],
|
||||
},
|
||||
querying: {
|
||||
invoke: {
|
||||
src: 'getCompletion',
|
||||
onDone: {
|
||||
target: 'success',
|
||||
actions: [ 'handleAiResponse' ],
|
||||
},
|
||||
onError: {
|
||||
target: 'error',
|
||||
},
|
||||
},
|
||||
},
|
||||
error: {
|
||||
always: [
|
||||
{
|
||||
cond: ( context ) => context.retryCount >= 3,
|
||||
target: 'failed',
|
||||
},
|
||||
{
|
||||
target: 'querying',
|
||||
actions: assign( {
|
||||
retryCount: ( context ) => context.retryCount + 1,
|
||||
} ),
|
||||
},
|
||||
],
|
||||
},
|
||||
failed: {
|
||||
type: 'final',
|
||||
data: {
|
||||
result: 'failed',
|
||||
},
|
||||
},
|
||||
success: {
|
||||
type: 'final',
|
||||
data: ( context ) => {
|
||||
return {
|
||||
result: 'success',
|
||||
response: context.validatedResponse,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
handleAiResponse: assign( {
|
||||
validatedResponse: ( _context, event: unknown ) =>
|
||||
( event as { data: unknown } ).data,
|
||||
} ),
|
||||
setRetryCount: assign( {
|
||||
retryCount: 0,
|
||||
} ),
|
||||
},
|
||||
services: {
|
||||
getCompletion,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const services = {
|
||||
getLookAndTone,
|
||||
browserPopstateHandler,
|
||||
queryAiEndpoint,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { getQuery } from '@woocommerce/navigation';
|
|||
import {
|
||||
designWithAiStateMachineContext,
|
||||
designWithAiStateMachineEvents,
|
||||
ColorPalette,
|
||||
} from './types';
|
||||
import {
|
||||
BusinessInfoDescription,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
} from './pages';
|
||||
import { actions } from './actions';
|
||||
import { services } from './services';
|
||||
import { defaultColorPalette } from './prompts';
|
||||
|
||||
export const hasStepInUrl = (
|
||||
_ctx: unknown,
|
||||
|
@ -60,13 +62,15 @@ export const designWithAiStateMachineDefinition = createMachine(
|
|||
businessInfoDescription: {
|
||||
descriptionText: '',
|
||||
},
|
||||
|
||||
lookAndFeel: {
|
||||
choice: '',
|
||||
},
|
||||
toneOfVoice: {
|
||||
choice: '',
|
||||
},
|
||||
aiSuggestions: {
|
||||
defaultColorPalette: {} as ColorPalette,
|
||||
},
|
||||
},
|
||||
initial: 'navigate',
|
||||
states: {
|
||||
|
@ -264,6 +268,30 @@ export const designWithAiStateMachineDefinition = createMachine(
|
|||
step: 'api-call-loader',
|
||||
},
|
||||
],
|
||||
type: 'parallel',
|
||||
states: {
|
||||
chooseColorPairing: {
|
||||
invoke: {
|
||||
src: 'queryAiEndpoint',
|
||||
data: ( context ) => {
|
||||
return {
|
||||
...defaultColorPalette,
|
||||
prompt: defaultColorPalette.prompt(
|
||||
context.businessInfoDescription
|
||||
.descriptionText,
|
||||
context.lookAndFeel.choice,
|
||||
context.toneOfVoice.choice
|
||||
),
|
||||
};
|
||||
},
|
||||
onDone: {
|
||||
actions: [
|
||||
'assignDefaultColorPalette',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
postApiCallLoader: {},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
.woocommerce-onboarding-loader {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
|
||||
.woocommerce-onboarding-loader-wrapper {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
max-width: 520px;
|
||||
min-height: 400px;
|
||||
|
||||
.woocommerce-onboarding-loader-container {
|
||||
text-align: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.loader-hearticon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,63 +2,147 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
|
||||
import apiFetch from '@wordpress/api-fetch';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
parseLookAndToneCompletionResponse,
|
||||
LookAndToneCompletionResponse,
|
||||
} from '../services';
|
||||
import { getCompletion } from '../services';
|
||||
|
||||
jest.mock( '@woocommerce/tracks', () => ( {
|
||||
recordEvent: jest.fn(),
|
||||
} ) );
|
||||
|
||||
describe( 'parseLookAndToneCompletionResponse', () => {
|
||||
jest.mock( '@woocommerce/ai', () => ( {
|
||||
__experimentalRequestJetpackToken: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
|
||||
|
||||
describe( 'getCompletion', () => {
|
||||
beforeEach( () => {
|
||||
( recordEvent as jest.Mock ).mockClear();
|
||||
jest.clearAllMocks();
|
||||
} );
|
||||
|
||||
it( 'should return a valid object when given valid JSON', () => {
|
||||
const validObj = {
|
||||
completion: '{"look": "Contemporary", "tone": "Neutral"}',
|
||||
};
|
||||
const result = parseLookAndToneCompletionResponse( validObj );
|
||||
const expected: LookAndToneCompletionResponse = {
|
||||
look: 'Contemporary',
|
||||
tone: 'Neutral',
|
||||
};
|
||||
expect( result ).toEqual( expected );
|
||||
expect( recordEvent ).not.toHaveBeenCalled();
|
||||
} );
|
||||
it( 'should successfully get completion', async () => {
|
||||
( requestJetpackToken as jest.Mock ).mockResolvedValue( {
|
||||
token: 'fake_token',
|
||||
} );
|
||||
( apiFetch as unknown as jest.Mock ).mockResolvedValue( {
|
||||
completion: JSON.stringify( { key: 'value' } ),
|
||||
} );
|
||||
const responseValidation = jest.fn( ( json ) => json );
|
||||
|
||||
it( 'should throw an error and record an event for JSON parse error', () => {
|
||||
const invalidObj = { completion: 'invalid JSON' };
|
||||
expect( () =>
|
||||
parseLookAndToneCompletionResponse( invalidObj )
|
||||
).toThrow( 'Could not parse Look and Tone completion response.' );
|
||||
expect( recordEvent ).toHaveBeenCalledWith(
|
||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
||||
const result = await getCompletion( {
|
||||
queryId: 'query1',
|
||||
prompt: 'test prompt',
|
||||
responseValidation,
|
||||
retryCount: 0,
|
||||
version: '1',
|
||||
} );
|
||||
|
||||
expect( result ).toEqual( { key: 'value' } );
|
||||
expect( responseValidation ).toBeCalledWith( { key: 'value' } );
|
||||
expect( recordEvent ).toBeCalledWith(
|
||||
'customize_your_store_ai_completion_success',
|
||||
{
|
||||
error_type: 'json_parse_error',
|
||||
response: JSON.stringify( invalidObj ),
|
||||
query_id: 'query1',
|
||||
retry_count: 0,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should throw an error and record an event for valid JSON but invalid values', () => {
|
||||
const invalidValuesObj = {
|
||||
completion: '{"look": "Invalid", "tone": "Invalid"}',
|
||||
};
|
||||
expect( () =>
|
||||
parseLookAndToneCompletionResponse( invalidValuesObj )
|
||||
).toThrow( 'Could not parse Look and Tone completion response.' );
|
||||
expect( recordEvent ).toHaveBeenCalledWith(
|
||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
||||
it( 'should handle API fetch error', async () => {
|
||||
( requestJetpackToken as jest.Mock ).mockResolvedValue( {
|
||||
token: 'fake_token',
|
||||
} );
|
||||
( apiFetch as unknown as jest.Mock ).mockRejectedValue(
|
||||
new Error( 'API error' )
|
||||
);
|
||||
|
||||
await expect(
|
||||
getCompletion( {
|
||||
queryId: 'query1',
|
||||
prompt: 'test prompt',
|
||||
responseValidation: () => {},
|
||||
retryCount: 0,
|
||||
version: '1',
|
||||
} )
|
||||
).rejects.toThrow( 'API error' );
|
||||
|
||||
expect( recordEvent ).toBeCalledWith(
|
||||
'customize_your_store_ai_completion_api_error',
|
||||
{
|
||||
query_id: 'query1',
|
||||
retry_count: 0,
|
||||
error_type: 'api_request_error',
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should handle JSON parse error', async () => {
|
||||
( requestJetpackToken as jest.Mock ).mockResolvedValue( {
|
||||
token: 'fake_token',
|
||||
} );
|
||||
( apiFetch as unknown as jest.Mock ).mockResolvedValue( {
|
||||
completion: 'invalid json',
|
||||
} );
|
||||
|
||||
await expect(
|
||||
getCompletion( {
|
||||
queryId: 'query1',
|
||||
prompt: 'test prompt',
|
||||
responseValidation: () => {},
|
||||
retryCount: 0,
|
||||
version: '1',
|
||||
} )
|
||||
).rejects.toThrow(
|
||||
`Error validating Jetpack AI text completions response for query1`
|
||||
);
|
||||
|
||||
expect( recordEvent ).toBeCalledWith(
|
||||
'customize_your_store_ai_completion_response_error',
|
||||
{
|
||||
query_id: 'query1',
|
||||
retry_count: 0,
|
||||
error_type: 'json_parse_error',
|
||||
response: 'invalid json',
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should handle validation error', async () => {
|
||||
( requestJetpackToken as jest.Mock ).mockResolvedValue( {
|
||||
token: 'fake_token',
|
||||
} );
|
||||
( apiFetch as unknown as jest.Mock ).mockResolvedValue( {
|
||||
completion: JSON.stringify( { key: 'invalid value' } ),
|
||||
} );
|
||||
const responseValidation = jest.fn( () => {
|
||||
throw new Error( 'Validation error' );
|
||||
} );
|
||||
|
||||
await expect(
|
||||
getCompletion( {
|
||||
queryId: 'query1',
|
||||
prompt: 'test prompt',
|
||||
responseValidation,
|
||||
retryCount: 0,
|
||||
version: '1',
|
||||
} )
|
||||
).rejects.toThrow( 'Validation error' );
|
||||
|
||||
expect( recordEvent ).toBeCalledWith(
|
||||
'customize_your_store_ai_completion_response_error',
|
||||
{
|
||||
query_id: 'query1',
|
||||
retry_count: 0,
|
||||
error_type: 'valid_json_invalid_values',
|
||||
response: JSON.stringify( invalidValuesObj ),
|
||||
response: JSON.stringify( { key: 'invalid value' } ),
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { colorPaletteValidator } from './prompts';
|
||||
|
||||
export type designWithAiStateMachineContext = {
|
||||
businessInfoDescription: {
|
||||
descriptionText: string;
|
||||
|
@ -8,6 +17,9 @@ export type designWithAiStateMachineContext = {
|
|||
toneOfVoice: {
|
||||
choice: Tone | '';
|
||||
};
|
||||
aiSuggestions: {
|
||||
defaultColorPalette: ColorPalette;
|
||||
};
|
||||
// If we require more data from options, previously provided core profiler details,
|
||||
// we can retrieve them in preBusinessInfoDescription and then assign them here
|
||||
};
|
||||
|
@ -31,3 +43,10 @@ export const VALID_LOOKS = [ 'Contemporary', 'Classic', 'Bold' ] as const;
|
|||
export const VALID_TONES = [ 'Informal', 'Neutral', 'Formal' ] as const;
|
||||
export type Look = ( typeof VALID_LOOKS )[ number ];
|
||||
export type Tone = ( typeof VALID_TONES )[ number ];
|
||||
|
||||
export interface LookAndToneCompletionResponse {
|
||||
look: Look;
|
||||
tone: Tone;
|
||||
}
|
||||
|
||||
export type ColorPalette = z.infer< typeof colorPaletteValidator >;
|
||||
|
|
|
@ -5,6 +5,8 @@ import { Sender, createMachine } from 'xstate';
|
|||
import { useEffect, useMemo, useState } from '@wordpress/element';
|
||||
import { useMachine, useSelector } from '@xstate/react';
|
||||
import { getQuery, updateQueryString } from '@woocommerce/navigation';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -57,6 +59,12 @@ const updateQueryStep = (
|
|||
}
|
||||
};
|
||||
|
||||
const markTaskComplete = async () => {
|
||||
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
|
||||
woocommerce_admin_customize_store_completed: 'yes',
|
||||
} );
|
||||
};
|
||||
|
||||
const browserPopstateHandler =
|
||||
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
|
||||
const popstateHandler = () => {
|
||||
|
@ -81,6 +89,7 @@ export const customizeStoreStateMachineServices = {
|
|||
...introServices,
|
||||
...transitionalServices,
|
||||
browserPopstateHandler,
|
||||
markTaskComplete,
|
||||
};
|
||||
export const customizeStoreStateMachineDefinition = createMachine( {
|
||||
id: 'customizeStore',
|
||||
|
@ -220,6 +229,14 @@ export const customizeStoreStateMachineDefinition = createMachine( {
|
|||
},
|
||||
},
|
||||
postAssemblerHub: {
|
||||
invoke: {
|
||||
src: 'markTaskComplete',
|
||||
onDone: {
|
||||
target: 'waitForSitePreview',
|
||||
},
|
||||
},
|
||||
},
|
||||
waitForSitePreview: {
|
||||
after: {
|
||||
// Wait for 5 seconds before redirecting to the transitional page. This is to ensure that the site preview image is refreshed.
|
||||
5000: {
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
&__content {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.components-snackbar__content-with-icon {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<svg width="74" height="100" viewBox="0 0 74 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M67.7896 0L62.6619 4.11855L57.5289 0L52.4011 4.11855L47.2682 0L42.1353 4.11855L37.0075 0L31.8746 4.11855L26.7468 0L21.6138 4.11855L16.4809 0L11.348 4.11855L6.21501 0L0.916992 4.25258V95.7474L6.20986 100L11.3428 95.8763L16.4706 100L21.6035 95.8763L26.7313 100L31.8642 95.8763L36.9972 100L42.125 95.8763L47.2579 100L52.3857 95.8763L57.5186 100L62.6515 95.8763L67.7896 100L73.0825 95.7474V4.25258L67.7896 0Z" fill="#E0E0E0"/>
|
||||
<path d="M49.4449 18.7216H7.10547V22.5103H49.4449V18.7216Z" fill="#757575"/>
|
||||
<path d="M49.4449 30.2784H7.10547V34.067H49.4449V30.2784Z" fill="#757575"/>
|
||||
<path d="M49.4449 41.8351H7.10547V45.6237H49.4449V41.8351Z" fill="#757575"/>
|
||||
<path d="M66.8991 18.7216H56.9102V22.5103H66.8991V18.7216Z" fill="white"/>
|
||||
<path d="M66.8991 30.2783H56.9102V34.067H66.8991V30.2783Z" fill="white"/>
|
||||
<path d="M66.8991 41.835H56.9102V45.6237H66.8991V41.835Z" fill="white"/>
|
||||
<path d="M66.8993 63.9176H50.4043V71.1341H66.8993V63.9176Z" fill="#757575"/>
|
||||
<path d="M7.13379 55.5258H66.8714" stroke="#271B3D" stroke-width="0.510311" stroke-miterlimit="10"/>
|
||||
<path d="M51.2154 89.8917C50.6639 89.8917 50.1845 89.7731 49.8082 89.4999C48.0556 88.2473 47.8597 85.768 47.6845 83.5772C47.4886 81.0618 47.2876 79.5205 45.736 79.5205C44.437 79.5205 43.5659 81.7731 42.7205 83.9484C41.705 86.5669 40.6535 89.2731 38.6122 89.2731C36.4473 89.2731 36.1895 87.1752 35.937 85.1494C35.6741 83.0205 35.4266 81.0051 33.2256 81.0051V80.4948C35.8802 80.4948 36.1792 82.9329 36.4473 85.0875C36.7308 87.3762 37.1586 88.4071 38.6071 88.4535C39.8648 88.4896 41.2875 86.2267 42.2411 83.768C43.1896 81.3247 44.2978 78.2886 45.9473 78.2886C48.0504 78.2886 48.2721 80.3762 48.4061 82.8195C48.5401 85.2628 48.5401 87.9741 50.102 89.0927C52.8546 91.0618 62.4011 83.2473 66.2259 79.4174L67.0558 80.3711C66.5609 80.804 56.0093 89.8968 51.2154 89.8968V89.8917Z" fill="#CCCCCC"/>
|
||||
</svg>
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2483_9556)">
|
||||
<path d="M15.9776 43.968C11.1189 43.968 5.34584 44.544 2.95838 46.928C2.95838 46.928 2.95838 46.928 2.95439 46.932C2.95439 46.932 2.95439 46.932 2.95039 46.936C0.574906 49.332 -9.13044e-07 55.112 -7.00082e-07 59.984C-4.8712e-07 64.856 0.574907 70.64 2.95039 73.032C2.95039 73.032 2.95039 73.032 2.95439 73.036C2.95439 73.036 2.95438 73.036 2.95838 73.04C5.34584 75.424 11.1189 76 15.9776 76C20.8364 76 26.6094 75.424 28.9969 73.04C28.9969 73.04 28.9969 73.04 29.0009 73.036C29.0049 73.032 29.0009 73.036 29.0049 73.032C31.3804 70.636 31.9553 64.856 31.9553 59.984C31.9553 55.112 31.3804 49.328 29.0049 46.936C29.0049 46.936 29.0049 46.936 29.0009 46.932C28.9969 46.928 29.0009 46.932 28.9969 46.928C26.6094 44.544 20.8364 43.968 15.9776 43.968Z" fill="#757575"/>
|
||||
<path d="M63.9776 3.968C59.1189 3.968 53.3458 4.544 50.9584 6.928C50.9584 6.928 50.9584 6.928 50.9544 6.932C50.9544 6.932 50.9544 6.932 50.9504 6.936C48.5749 9.332 48 15.112 48 19.984C48 24.856 48.5749 30.64 50.9504 33.032C50.9504 33.032 50.9504 33.032 50.9544 33.036C50.9544 33.036 50.9544 33.036 50.9584 33.04C53.3458 35.424 59.1189 36 63.9776 36C68.8364 36 74.6094 35.424 76.9969 33.04C76.9969 33.04 76.9969 33.04 77.0009 33.036C77.0049 33.032 77.0009 33.036 77.0049 33.032C79.3804 30.636 79.9553 24.856 79.9553 19.984C79.9553 15.112 79.3804 9.328 77.0049 6.936C77.0049 6.936 77.0049 6.936 77.0009 6.932C76.9969 6.928 77.0009 6.932 76.9969 6.92799C74.6094 4.544 68.8364 3.968 63.9776 3.968Z" fill="#757575"/>
|
||||
<path d="M40 60C40 64.8656 40.7193 70.6467 43.6963 73.0375C43.6963 73.0375 43.6963 73.0375 43.7013 73.0415C43.7013 73.0415 43.7013 73.0415 43.7063 73.0455C46.6983 75.4243 53.9161 76 60 76C66.0839 76 73.3067 75.4243 76.2937 73.0455C76.2937 73.0455 76.2937 73.0455 76.2987 73.0415C76.2987 73.0415 76.2987 73.0415 76.3037 73.0375C79.2807 70.6467 80 64.8656 80 60C80 55.1344 79.2807 49.3533 76.3037 46.9625C76.3037 46.9625 76.3037 46.9625 76.2987 46.9585C76.2937 46.9545 76.2987 46.9585 76.2937 46.9545C73.3017 44.5757 66.0839 44 60 44C53.9161 44 46.6933 44.5757 43.7063 46.9545C43.7063 46.9545 43.7063 46.9545 43.7013 46.9585C43.6963 46.9625 43.7013 46.9585 43.6963 46.9625C40.7193 49.3533 40 55.1344 40 60Z" fill="#E0E0E0"/>
|
||||
<path d="M-1.39876e-06 20C-9.73403e-07 24.8656 0.719276 30.6467 3.6963 33.0375C3.6963 33.0375 3.6963 33.0375 3.7013 33.0415C3.7013 33.0415 3.7013 33.0415 3.70629 33.0455C6.6983 35.4243 13.9161 36 20 36C26.0839 36 33.3067 35.4243 36.2937 33.0455C36.2937 33.0455 36.2937 33.0455 36.2987 33.0415C36.2987 33.0415 36.2987 33.0415 36.3037 33.0375C39.2807 30.6467 40 24.8656 40 20C40 15.1344 39.2807 9.35332 36.3037 6.96251C36.3037 6.96251 36.3037 6.96251 36.2987 6.95852C36.2937 6.95452 36.2987 6.95852 36.2937 6.95452C33.3017 4.57571 26.0839 4 20 4C13.9161 4 6.6933 4.57571 3.70629 6.95452C3.70629 6.95452 3.70629 6.95452 3.7013 6.95852C3.6963 6.96252 3.70129 6.95852 3.6963 6.96252C0.719274 9.35332 -1.82413e-06 15.1344 -1.39876e-06 20Z" fill="#E0E0E0"/>
|
||||
<path d="M38.5095 14.0378C40.9729 11.6218 45.2004 11.3695 45.2004 10.4C45.2004 9.43044 40.9729 9.1782 38.5095 6.76221C36.0461 4.34621 35.789 0.199998 34.8004 0.199998C33.8118 0.199998 33.5546 4.34621 31.0913 6.76221C28.6279 9.1782 24.4004 9.43044 24.4004 10.4C24.4004 11.3695 28.6279 11.6218 31.0913 14.0378C33.5546 16.4538 33.8118 20.6 34.8004 20.6C35.789 20.6 36.0461 16.4538 38.5095 14.0378Z" fill="#757575"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2483_9556">
|
||||
<rect width="80" height="80" fill="white" transform="translate(0 80) rotate(-90)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
@ -4,11 +4,11 @@
|
|||
box-sizing: content-box;
|
||||
margin: auto;
|
||||
max-width: $content-max-width;
|
||||
padding: $header-height-mobile $content-spacing-small $content-spacing-small;
|
||||
padding: 0 $content-spacing-small $content-spacing-small;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $breakpoint-medium) {
|
||||
.woocommerce-marketplace__content {
|
||||
padding: $header-height-desktop $content-spacing-large $content-spacing-large;
|
||||
padding: 0 $content-spacing-large $content-spacing-large;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,12 @@ export default function Extensions(): JSX.Element {
|
|||
return <NoResults />;
|
||||
}
|
||||
|
||||
return <ProductListContent products={ products } />;
|
||||
return (
|
||||
<>
|
||||
<CategorySelector />
|
||||
<ProductListContent products={ products } />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -120,7 +125,6 @@ export default function Extensions(): JSX.Element {
|
|||
<h2 className="woocommerce-marketplace__product-list-title woocommerce-marketplace__product-list-title--extensions">
|
||||
{ title }
|
||||
</h2>
|
||||
<CategorySelector />
|
||||
{ content() }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ import WooIcon from '../../assets/images/woo-icon.svg';
|
|||
import { MARKETPLACE_HOST } from '../constants';
|
||||
|
||||
const refundPolicyTitle = createInterpolateElement(
|
||||
__( '30 day <a>money back guarantee</a>', 'woocommerce' ),
|
||||
__( '30-day <a>money-back guarantee</a>', 'woocommerce' ),
|
||||
{
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
a: <a href={ MARKETPLACE_HOST + '/refund-policy/' } />,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
padding: $grid-unit-80 $grid-unit-40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: $grid-unit-30;
|
||||
}
|
||||
|
||||
|
@ -13,12 +14,13 @@
|
|||
}
|
||||
|
||||
.woocommerce-marketplace__no-results__icon {
|
||||
height: 100px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.woocommerce-marketplace__no-results__description {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
max-width: 52ch;
|
||||
|
||||
p {
|
||||
color: $gutenberg-gray-700;
|
||||
|
|
|
@ -90,13 +90,15 @@ export default function NoResults(): JSX.Element {
|
|||
className="woocommerce-marketplace__no-results__icon"
|
||||
src={ NoResultsIcon }
|
||||
alt={ __( 'No results.', 'woocommerce' ) }
|
||||
width="80"
|
||||
height="80"
|
||||
/>
|
||||
<div className="woocommerce-marketplace__no-results__description">
|
||||
<h3 className="woocommerce-marketplace__no-results__description--bold">
|
||||
{ sprintf(
|
||||
// translators: %s: search term
|
||||
__(
|
||||
'We didn\'t find any results for "%s"',
|
||||
"We didn't find any results for “%s”",
|
||||
'woocommerce'
|
||||
),
|
||||
noResultsTerm
|
||||
|
|
|
@ -22,12 +22,14 @@ export default function ProductListContent( props: {
|
|||
title: product.title,
|
||||
icon: product.icon,
|
||||
vendorName: product.vendorName,
|
||||
vendorUrl: appendURLParams( product.vendorUrl, [
|
||||
[ 'utm_source', 'extensionsscreen' ],
|
||||
[ 'utm_medium', 'product' ],
|
||||
[ 'utm_campaign', 'wcaddons' ],
|
||||
[ 'utm_content', 'devpartner' ],
|
||||
] ),
|
||||
vendorUrl: product.vendorUrl
|
||||
? appendURLParams( product.vendorUrl, [
|
||||
[ 'utm_source', 'extensionsscreen' ],
|
||||
[ 'utm_medium', 'product' ],
|
||||
[ 'utm_campaign', 'wcaddons' ],
|
||||
[ 'utm_content', 'devpartner' ],
|
||||
] )
|
||||
: '',
|
||||
price: product.price,
|
||||
url: appendURLParams(
|
||||
product.url,
|
||||
|
|
|
@ -3,11 +3,31 @@
|
|||
.woocommerce-admin-page__extensions {
|
||||
background: #fff;
|
||||
|
||||
#wpbody-content {
|
||||
/* Prevent double-scrollbar issue on WooCommerce > Extension pages */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.woocommerce-layout__primary {
|
||||
margin: 0;
|
||||
margin: $header-height-mobile 0 0;
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
margin-top: $header-height-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-layout__main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* On marketplace pages, reposition store alerts so they don't collide with other components */
|
||||
.woocommerce-store-alerts {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
margin-left: 32px;
|
||||
margin-right: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export const ProductFormActions: React.FC = () => {
|
|||
const { isDirty, isValidForm, values, resetForm } =
|
||||
useFormContext< Product >();
|
||||
|
||||
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm );
|
||||
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm() );
|
||||
|
||||
useCustomerEffortScoreExitPageTracker(
|
||||
! values.id ? 'new_product' : 'editing_new_product',
|
||||
|
|
|
@ -31,7 +31,7 @@ export const ProductVariationFormActions: React.FC = () => {
|
|||
const { createNotice } = useDispatch( 'core/notices' );
|
||||
const [ isSaving, setIsSaving ] = useState( false );
|
||||
|
||||
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm );
|
||||
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm() );
|
||||
|
||||
const onSave = async () => {
|
||||
setIsSaving( true );
|
||||
|
|
|
@ -44,6 +44,7 @@ jest.mock( '@woocommerce/product-editor', () => {
|
|||
__experimentalUseFeedbackBar: () => ( {
|
||||
maybeShowFeedbackBar: jest.fn().mockResolvedValue( {} ),
|
||||
} ),
|
||||
preventLeavingProductForm: () => () => false,
|
||||
};
|
||||
} );
|
||||
jest.mock( '@woocommerce/navigation', () => ( {
|
||||
|
|
|
@ -101,7 +101,8 @@
|
|||
"react-transition-group": "^4.4.2",
|
||||
"react-visibility-sensor": "^5.1.1",
|
||||
"redux": "^4.1.2",
|
||||
"xstate": "4.37.1"
|
||||
"xstate": "4.37.1",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automattic/color-studio": "^2.5.0",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add component to Customize Your Store task.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add has_price param to the variations REST API query.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add customize store AI wizard call for color palette suggestion
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add tracks to CYS assembler-hub and hide pages sidebar screen
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add new e2e test to cover My Account Addresses section
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Add new E2E test covering shopper product page and make Product-related tests granular (separated test files)
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Add job to post Slack summary of plugin test results in "Smoke test daily" workflow.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Display search results subtitle in HPOS list table view.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Properly convert local time date queries to UTC in the HPOS datastore.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Update use of preventLeavingProductForm with new function changes.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: dev
|
||||
|
||||
Improve documentation for the `is_checkout()` function.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: Tweak the spelling of the marketplace footer
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Redirect to Jetpack connect when jetpack-boost is selected.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Improved visibility/layout of store alerts on WooCommerce > Extensions pages.
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Fixed a bug that caused redundant extra scrollbars to appear on WooCommerce > Extensions pages for some browser/OS/configuration combinations.
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: More-resilient handling of absent product vendor URLs when browsing WooCommerce > Extensions.
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Implement customize your store task completion logic
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Remove core-profiler checks from the tests -- core profiler is enabled by default now.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: Update the design of the extensions search "no results" page.
|
||||
|
||||
|
|
@ -866,6 +866,17 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
|
|||
$args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok.
|
||||
}
|
||||
|
||||
// Price filter.
|
||||
if ( is_bool( $request['has_price'] ) ) {
|
||||
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
|
||||
$args,
|
||||
array(
|
||||
'key' => '_price',
|
||||
'compare' => $request['has_price'] ? 'EXISTS' : 'NOT EXISTS',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter product based on stock_status.
|
||||
if ( ! empty( $request['stock_status'] ) ) {
|
||||
$args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok.
|
||||
|
@ -927,6 +938,13 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
|
|||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
|
||||
$params['has_price'] = array(
|
||||
'description' => __( 'Limit result set to products with or without price.', 'woocommerce' ),
|
||||
'type' => 'boolean',
|
||||
'sanitize_callback' => 'wc_string_to_bool',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ if ( ! function_exists( 'is_cart' ) ) {
|
|||
if ( ! function_exists( 'is_checkout' ) ) {
|
||||
|
||||
/**
|
||||
* Is_checkout - Returns true when viewing the checkout page.
|
||||
* Is_checkout - Returns true when viewing the checkout page, or when processing AJAX requests for updating or processing the checkout.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
|
|
@ -214,6 +214,7 @@ class Options extends \WC_REST_Data_Controller {
|
|||
'wcpay_welcome_page_viewed_timestamp',
|
||||
'wcpay_welcome_page_exit_survey_more_info_needed_timestamp',
|
||||
'woocommerce_customize_store_onboarding_tour_hidden',
|
||||
'woocommerce_admin_customize_store_completed',
|
||||
// WC Test helper options.
|
||||
'wc-admin-test-helper-rest-api-filters',
|
||||
'wc_admin_helper_feature_values',
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
|
||||
use Jetpack_Gutenberg;
|
||||
|
||||
/**
|
||||
* Customize Your Store Task
|
||||
|
@ -15,7 +16,9 @@ class CustomizeStore extends Task {
|
|||
*/
|
||||
public function __construct( $task_list ) {
|
||||
parent::__construct( $task_list );
|
||||
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_site_editor_scripts' ) );
|
||||
add_action( 'after_switch_theme', array( $this, 'mark_task_as_complete' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -182,5 +185,17 @@ class CustomizeStore extends Task {
|
|||
* @since 8.0.3
|
||||
*/
|
||||
do_action( 'enqueue_block_editor_assets' );
|
||||
|
||||
// Load Jetpack's block editor assets because they are not enqueued by default.
|
||||
if ( class_exists( 'Jetpack_Gutenberg' ) ) {
|
||||
Jetpack_Gutenberg::enqueue_block_editor_assets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark task as complete.
|
||||
*/
|
||||
public function mark_task_as_complete() {
|
||||
update_option( 'woocommerce_admin_customize_store_completed', 'yes' );
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue