Merge branch 'trunk' into feature/marketplace-subscriptions
This commit is contained in:
commit
69929ba050
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 }}
|
API_ARTIFACT: api-daily--run-${{ github.run_number }}
|
||||||
E2E_ARTIFACT: e2e-daily--run-${{ github.run_number }}
|
E2E_ARTIFACT: e2e-daily--run-${{ github.run_number }}
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
PLUGIN_SLACK_BLOCKS_ARTIFACT: plugin-blocks
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
@ -21,6 +22,8 @@ jobs:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
outputs:
|
||||||
|
test-result: ${{ steps.run-api-composite-action.outputs.result }}
|
||||||
env:
|
env:
|
||||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results
|
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
|
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report
|
||||||
|
@ -42,38 +45,31 @@ jobs:
|
||||||
install-filters: woocommerce
|
install-filters: woocommerce
|
||||||
build: false
|
build: false
|
||||||
|
|
||||||
- name: Download and install Chromium browser.
|
- name: Update site to nightly version
|
||||||
working-directory: plugins/woocommerce
|
uses: ./.github/actions/tests/run-e2e-tests
|
||||||
run: pnpm exec playwright install chromium
|
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.
|
- name: Run API tests
|
||||||
working-directory: plugins/woocommerce
|
id: run-api-composite-action
|
||||||
|
uses: ./.github/actions/tests/run-api-tests
|
||||||
|
with:
|
||||||
|
report-name: ${{ env.API_ARTIFACT }}
|
||||||
env:
|
env:
|
||||||
USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
|
USER_KEY: ${{ secrets.SMOKE_TEST_ADMIN_USER }}
|
||||||
USER_SECRET: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
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:
|
e2e-tests:
|
||||||
name: E2E tests on nightly build
|
name: E2E tests on nightly build
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
outputs:
|
||||||
|
test-result: ${{ steps.run-e2e-composite-action.outputs.result }}
|
||||||
# needs: [api-tests]
|
# needs: [api-tests]
|
||||||
env:
|
env:
|
||||||
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_ADMIN_PASSWORD }}
|
||||||
|
@ -85,7 +81,6 @@ jobs:
|
||||||
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
|
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_CUSTOMER_PASSWORD }}
|
||||||
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
|
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_CUSTOMER_USER }}
|
||||||
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
@ -95,33 +90,24 @@ jobs:
|
||||||
install-filters: woocommerce
|
install-filters: woocommerce
|
||||||
build: false
|
build: false
|
||||||
|
|
||||||
- name: Download and install Chromium browser.
|
- name: Run E2E tests
|
||||||
working-directory: plugins/woocommerce
|
id: run-e2e-composite-action
|
||||||
run: pnpm exec playwright install chromium
|
|
||||||
|
|
||||||
- name: Run E2E tests.
|
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
working-directory: plugins/woocommerce
|
uses: ./.github/actions/tests/run-e2e-tests
|
||||||
|
with:
|
||||||
|
report-name: ${{ env.E2E_ARTIFACT }}
|
||||||
env:
|
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
|
E2E_MAX_FAILURES: 25
|
||||||
RESET_SITE: true
|
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:
|
k6-tests:
|
||||||
name: k6 tests on nightly build
|
name: k6 tests on nightly build
|
||||||
|
@ -130,6 +116,8 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
needs: [api-tests]
|
needs: [api-tests]
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
|
outputs:
|
||||||
|
test-result: ${{ steps.run-k6-tests.conclusion }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
@ -139,29 +127,31 @@ jobs:
|
||||||
install-filters: woocommerce
|
install-filters: woocommerce
|
||||||
build: false
|
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
|
- 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:
|
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 }}/
|
BASE_URL: ${{ secrets.SMOKE_TEST_PERF_URL }}/
|
||||||
ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
ADMIN_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
||||||
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
ADMIN_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
||||||
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
CUSTOMER_USER: ${{ secrets.SMOKE_TEST_PERF_ADMIN_USER }}
|
||||||
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
CUSTOMER_PASSWORD: ${{ secrets.SMOKE_TEST_PERF_ADMIN_PASSWORD }}
|
||||||
UPDATE_WC: nightly
|
|
||||||
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
DEFAULT_TIMEOUT_OVERRIDE: 120000
|
||||||
run: |
|
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||||
pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js update-woocommerce.spec.js
|
UPDATE_WC: nightly
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Install k6
|
- name: Install k6
|
||||||
run: |
|
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
|
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
|
- name: Run k6 smoke tests
|
||||||
|
id: run-k6-tests
|
||||||
env:
|
env:
|
||||||
URL: ${{ secrets.SMOKE_TEST_PERF_URL }}
|
URL: ${{ secrets.SMOKE_TEST_PERF_URL }}
|
||||||
HOST: ${{ secrets.SMOKE_TEST_PERF_HOST }}
|
HOST: ${{ secrets.SMOKE_TEST_PERF_HOST }}
|
||||||
|
@ -180,7 +170,6 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
needs: [api-tests]
|
needs: [api-tests]
|
||||||
env:
|
env:
|
||||||
USE_WP_ENV: 1
|
|
||||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
|
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-results
|
||||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
|
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/allure-report
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -189,17 +178,23 @@ jobs:
|
||||||
include:
|
include:
|
||||||
- plugin: 'WooCommerce Payments'
|
- plugin: 'WooCommerce Payments'
|
||||||
repo: 'automattic/woocommerce-payments'
|
repo: 'automattic/woocommerce-payments'
|
||||||
|
slug: woocommerce-payments
|
||||||
- plugin: 'WooCommerce PayPal Payments'
|
- plugin: 'WooCommerce PayPal Payments'
|
||||||
repo: 'woocommerce/woocommerce-paypal-payments'
|
repo: 'woocommerce/woocommerce-paypal-payments'
|
||||||
|
slug: woocommerce-paypal-payments
|
||||||
- plugin: 'WooCommerce Shipping & Tax'
|
- plugin: 'WooCommerce Shipping & Tax'
|
||||||
repo: 'automattic/woocommerce-services'
|
repo: 'automattic/woocommerce-services'
|
||||||
|
slug: woocommerce-services
|
||||||
- plugin: 'WooCommerce Subscriptions'
|
- plugin: 'WooCommerce Subscriptions'
|
||||||
repo: WC_SUBSCRIPTIONS_REPO
|
repo: WC_SUBSCRIPTIONS_REPO
|
||||||
private: true
|
private: true
|
||||||
|
slug: woocommerce-subscriptions
|
||||||
- plugin: 'Gutenberg'
|
- plugin: 'Gutenberg'
|
||||||
repo: 'WordPress/gutenberg'
|
repo: 'WordPress/gutenberg'
|
||||||
|
slug: gutenberg
|
||||||
- plugin: 'Gutenberg - Nightly'
|
- plugin: 'Gutenberg - Nightly'
|
||||||
repo: 'bph/gutenberg'
|
repo: 'bph/gutenberg'
|
||||||
|
slug: gutenberg-nightly
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
@ -208,43 +203,52 @@ jobs:
|
||||||
with:
|
with:
|
||||||
build-filters: woocommerce
|
build-filters: woocommerce
|
||||||
|
|
||||||
- name: Launch wp-env e2e environment
|
- name: Setup local test environment
|
||||||
working-directory: plugins/woocommerce
|
uses: ./.github/actions/tests/setup-local-test-environment
|
||||||
run: pnpm env:test --filter=woocommerce
|
with:
|
||||||
|
test-type: e2e
|
||||||
- name: Download and install Chromium browser.
|
|
||||||
working-directory: plugins/woocommerce
|
|
||||||
run: pnpm exec playwright install chromium
|
|
||||||
|
|
||||||
- name: Run 'Upload plugin' test
|
- 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:
|
env:
|
||||||
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
|
PLUGIN_REPOSITORY: ${{ matrix.private && secrets[matrix.repo] || matrix.repo }}
|
||||||
PLUGIN_NAME: ${{ matrix.plugin }}
|
PLUGIN_NAME: ${{ matrix.plugin }}
|
||||||
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.E2E_GH_TOKEN }}
|
||||||
run: pnpm test:e2e-pw upload-plugin.spec.js
|
|
||||||
|
|
||||||
- name: Run the rest of E2E tests
|
- 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:
|
env:
|
||||||
E2E_MAX_FAILURES: 15
|
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()
|
if: success() || failure()
|
||||||
working-directory: plugins/woocommerce
|
id: create-block-json
|
||||||
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
|
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()
|
if: success() || failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Smoke tests on trunk with ${{ matrix.plugin }} plugin installed (run ${{ github.run_number }})
|
name: ${{ env.PLUGIN_SLACK_BLOCKS_ARTIFACT }}
|
||||||
path: |
|
path: ${{ steps.create-block-json.outputs.path }}
|
||||||
${{ env.ALLURE_RESULTS_DIR }}
|
|
||||||
${{ env.ALLURE_REPORT_DIR }}
|
|
||||||
if-no-files-found: ignore
|
|
||||||
retention-days: 5
|
|
||||||
|
|
||||||
trunk-results:
|
trunk-results:
|
||||||
name: Publish report on smoke tests on nightly build
|
name: Publish report on smoke tests on nightly build
|
||||||
|
@ -307,6 +311,7 @@ jobs:
|
||||||
name: Publish report on Smoke tests on trunk with plugins
|
name: Publish report on Smoke tests on trunk with plugins
|
||||||
if: |
|
if: |
|
||||||
( success() || failure() ) &&
|
( success() || failure() ) &&
|
||||||
|
( needs.test-plugins.result != 'skipped' ) &&
|
||||||
! github.event.pull_request.head.repo.fork
|
! github.event.pull_request.head.repo.fork
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
needs: [e2e-tests, test-plugins, k6-tests]
|
needs: [e2e-tests, test-plugins, k6-tests]
|
||||||
|
@ -345,3 +350,51 @@ jobs:
|
||||||
-f slug="${{ matrix.slug }}" \
|
-f slug="${{ matrix.slug }}" \
|
||||||
-f s3_root=public \
|
-f s3_root=public \
|
||||||
--repo woocommerce/woocommerce-test-reports
|
--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 }}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 8.1.1 2023-09-18 =
|
||||||
|
|
||||||
|
**WooCommerce**
|
||||||
|
|
||||||
|
* Fix - Do not send user meta data back in `woocommerce_get_customer_details` response. [#40221](https://github.com/woocommerce/woocommerce/pull/40221)
|
||||||
|
* Fix - Fix possible metadata duplication when HPOS is enabled. [#40148](https://github.com/woocommerce/woocommerce/pull/40148)
|
||||||
|
|
||||||
= 8.1.0 2023-09-12 =
|
= 8.1.0 2023-09-12 =
|
||||||
|
|
||||||
**WooCommerce**
|
**WooCommerce**
|
||||||
|
|
|
@ -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 { createElement, useEffect, useState } from '@wordpress/element';
|
||||||
import { useInstanceId } from '@wordpress/compose';
|
import { useInstanceId } from '@wordpress/compose';
|
||||||
import { BaseControl, TextControl } from '@wordpress/components';
|
import { BaseControl, TextControl } from '@wordpress/components';
|
||||||
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -16,6 +17,7 @@ import { SelectedItems } from '../experimental-select-control/selected-items';
|
||||||
import { ComboBox } from '../experimental-select-control/combo-box';
|
import { ComboBox } from '../experimental-select-control/combo-box';
|
||||||
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
|
import { SuffixIcon } from '../experimental-select-control/suffix-icon';
|
||||||
import { SelectTreeMenu } from './select-tree-menu';
|
import { SelectTreeMenu } from './select-tree-menu';
|
||||||
|
import { escapeHTML } from '../utils';
|
||||||
|
|
||||||
interface SelectTreeProps extends TreeControlProps {
|
interface SelectTreeProps extends TreeControlProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -185,11 +187,11 @@ export const SelectTree = function SelectTree( {
|
||||||
) : (
|
) : (
|
||||||
<TextControl
|
<TextControl
|
||||||
{ ...inputProps }
|
{ ...inputProps }
|
||||||
value={ props.createValue || '' }
|
value={ decodeEntities( props.createValue || '' ) }
|
||||||
onChange={ ( value ) => {
|
onChange={ ( value ) => {
|
||||||
if ( onInputChange ) onInputChange( value );
|
if ( onInputChange ) onInputChange( value );
|
||||||
const item = items.find(
|
const item = items.find(
|
||||||
( i ) => i.label === value
|
( i ) => i.label === escapeHTML( value )
|
||||||
);
|
);
|
||||||
if ( props.onSelect && item ) {
|
if ( props.onSelect && item ) {
|
||||||
props.onSelect( item );
|
props.onSelect( item );
|
||||||
|
|
|
@ -84,7 +84,7 @@ export { DynamicForm } from './dynamic-form';
|
||||||
export { default as TourKit } from './tour-kit';
|
export { default as TourKit } from './tour-kit';
|
||||||
export * as TourKitTypes from './tour-kit/types';
|
export * as TourKitTypes from './tour-kit/types';
|
||||||
export { CollapsibleContent } from './collapsible-content';
|
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 { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
|
||||||
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';
|
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';
|
||||||
export { WooProductTabItem as __experimentalWooProductTabItem } from './woo-product-tab-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>;
|
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;
|
variable_product_block_tour_shown?: string;
|
||||||
variations_report_columns?: string;
|
variations_report_columns?: string;
|
||||||
product_block_variable_options_notice_dismissed?: string;
|
product_block_variable_options_notice_dismissed?: string;
|
||||||
|
variable_items_without_price_notice_dismissed?: Record< number, string >;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WoocommerceMeta = UserPreferences & {
|
export type WoocommerceMeta = UserPreferences & {
|
||||||
task_list_tracked_started_tasks?: string;
|
task_list_tracked_started_tasks?: string;
|
||||||
|
variable_items_without_price_notice_dismissed?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WCUser<
|
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: dev
|
||||||
|
|
||||||
|
Update copy in the add variation options modal
|
|
@ -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,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Fix blocks product editor variation actions styles
|
|
@ -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 { useState } from '@wordpress/element';
|
||||||
import { resolveSelect } from '@wordpress/data';
|
import { resolveSelect } from '@wordpress/data';
|
||||||
|
import { escapeHTML } from '@woocommerce/components';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -61,7 +62,7 @@ const useTaxonomySearch = (
|
||||||
Taxonomy[]
|
Taxonomy[]
|
||||||
>( 'taxonomy', taxonomyName, {
|
>( 'taxonomy', taxonomyName, {
|
||||||
per_page: PAGINATION_SIZE,
|
per_page: PAGINATION_SIZE,
|
||||||
search,
|
search: escapeHTML( search ),
|
||||||
} );
|
} );
|
||||||
if ( options?.fetchParents ) {
|
if ( options?.fetchParents ) {
|
||||||
taxonomies = await getTaxonomiesMissingParents(
|
taxonomies = await getTaxonomiesMissingParents(
|
||||||
|
|
|
@ -1,16 +1,32 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* 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 { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { BlockEditProps } from '@wordpress/blocks';
|
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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { VariationsTable } from '../../components/variations-table';
|
import { VariationsTable } from '../../components/variations-table';
|
||||||
|
import { useValidation } from '../../contexts/validation-context';
|
||||||
import { VariationOptionsBlockAttributes } from './types';
|
import { VariationOptionsBlockAttributes } from './types';
|
||||||
import { VariableProductTour } from './variable-product-tour';
|
import { VariableProductTour } from './variable-product-tour';
|
||||||
|
import { TRACKS_SOURCE } from '../../constants';
|
||||||
|
import { handlePrompt } from '../../utils/handle-prompt';
|
||||||
|
|
||||||
export function Edit( {
|
export function Edit( {
|
||||||
context,
|
context,
|
||||||
|
@ -19,11 +35,168 @@ export function Edit( {
|
||||||
isInSelectedTab?: boolean;
|
isInSelectedTab?: boolean;
|
||||||
};
|
};
|
||||||
} ) {
|
} ) {
|
||||||
|
const noticeDimissed = useRef( false );
|
||||||
|
const { invalidateResolution } = useDispatch(
|
||||||
|
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||||
|
);
|
||||||
|
const productId = useEntityId( 'postType', 'product' );
|
||||||
const blockProps = useBlockProps();
|
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 (
|
return (
|
||||||
<div { ...blockProps }>
|
<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 /> }
|
{ context?.isInSelectedTab && <VariableProductTour /> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -130,21 +130,10 @@ export function Edit() {
|
||||||
'Add variation options',
|
'Add variation options',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
newAttributeModalDescription: createInterpolateElement(
|
newAttributeModalDescription: __(
|
||||||
__(
|
'Select from existing attributes or create new ones to add new variations for your product. You can change the order later.',
|
||||||
'Select from existing <globalAttributeLink>global attributes</globalAttributeLink> or create options for buyers to choose on the product page. You can change the order later.',
|
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
{
|
|
||||||
globalAttributeLink: (
|
|
||||||
<Link
|
|
||||||
href="https://woocommerce.com/document/variable-product/#add-attributes-to-use-for-variations"
|
|
||||||
type="external"
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
attributeRemoveLabel: __(
|
attributeRemoveLabel: __(
|
||||||
'Remove variation option',
|
'Remove variation option',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
|
|
|
@ -4,14 +4,9 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { BlockEditProps } from '@wordpress/blocks';
|
import type { BlockEditProps } from '@wordpress/blocks';
|
||||||
import { Button } from '@wordpress/components';
|
import { Button } from '@wordpress/components';
|
||||||
import { Link } from '@woocommerce/components';
|
|
||||||
import { Product, ProductAttribute } from '@woocommerce/data';
|
import { Product, ProductAttribute } from '@woocommerce/data';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import {
|
import { createElement, useState } from '@wordpress/element';
|
||||||
createElement,
|
|
||||||
useState,
|
|
||||||
createInterpolateElement,
|
|
||||||
} from '@wordpress/element';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
useBlockProps,
|
useBlockProps,
|
||||||
|
@ -131,20 +126,9 @@ export function Edit( {
|
||||||
{ isNewModalVisible && (
|
{ isNewModalVisible && (
|
||||||
<NewAttributeModal
|
<NewAttributeModal
|
||||||
title={ __( 'Add variation options', 'woocommerce' ) }
|
title={ __( 'Add variation options', 'woocommerce' ) }
|
||||||
description={ createInterpolateElement(
|
description={ __(
|
||||||
__(
|
'Select from existing attributes or create new ones to add new variations for your product. You can change the order later.',
|
||||||
'Select from existing <globalAttributeLink>global attributes</globalAttributeLink> or create options for buyers to choose on the product page. You can change the order later.',
|
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
|
||||||
{
|
|
||||||
globalAttributeLink: (
|
|
||||||
<Link
|
|
||||||
href="https://woocommerce.com/document/variable-product/#add-attributes-to-use-for-variations"
|
|
||||||
type="external"
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
) }
|
) }
|
||||||
createNewAttributesAsGlobal={ true }
|
createNewAttributesAsGlobal={ true }
|
||||||
notice={ '' }
|
notice={ '' }
|
||||||
|
|
|
@ -403,7 +403,9 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
||||||
}
|
}
|
||||||
value={
|
value={
|
||||||
attribute ===
|
attribute ===
|
||||||
null
|
null ||
|
||||||
|
attribute ===
|
||||||
|
undefined
|
||||||
? []
|
? []
|
||||||
: attribute.terms
|
: attribute.terms
|
||||||
}
|
}
|
||||||
|
@ -506,7 +508,9 @@ export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
|
||||||
label={ addAccessibleLabel }
|
label={ addAccessibleLabel }
|
||||||
disabled={
|
disabled={
|
||||||
values.attributes.length === 1 &&
|
values.attributes.length === 1 &&
|
||||||
values.attributes[ 0 ] === null
|
( values.attributes[ 0 ] === null ||
|
||||||
|
values.attributes[ 0 ] ===
|
||||||
|
undefined )
|
||||||
}
|
}
|
||||||
onClick={ () =>
|
onClick={ () =>
|
||||||
onAddingAttributes( values )
|
onAddingAttributes( values )
|
||||||
|
|
|
@ -89,7 +89,9 @@ export const AttributeListItem: React.FC< AttributeListItemProps > = ( {
|
||||||
position="top center"
|
position="top center"
|
||||||
text={ NOT_VISIBLE_TEXT }
|
text={ NOT_VISIBLE_TEXT }
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
<HiddenWithHelpIcon />
|
<HiddenWithHelpIcon />
|
||||||
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) }
|
) }
|
||||||
{ typeof onEditClick === 'function' && (
|
{ typeof onEditClick === 'function' && (
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function usePublish( {
|
||||||
onPublishSuccess?( product: Product ): void;
|
onPublishSuccess?( product: Product ): void;
|
||||||
onPublishError?( error: WPError ): void;
|
onPublishError?( error: WPError ): void;
|
||||||
} ): Button.ButtonProps {
|
} ): Button.ButtonProps {
|
||||||
const { isValidating, validate } = useValidations();
|
const { isValidating, validate } = useValidations< Product >();
|
||||||
|
|
||||||
const [ productId ] = useEntityProp< number >(
|
const [ productId ] = useEntityProp< number >(
|
||||||
'postType',
|
'postType',
|
||||||
|
@ -61,7 +61,9 @@ export function usePublish( {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await validate();
|
await validate( {
|
||||||
|
status: 'publish',
|
||||||
|
} );
|
||||||
|
|
||||||
// The publish button click not only change the status of the product
|
// 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
|
// but also save all the pending changes. So even if the status is
|
||||||
|
@ -93,6 +95,12 @@ export function usePublish( {
|
||||||
? 'product_publish_error'
|
? 'product_publish_error'
|
||||||
: 'product_create_error',
|
: 'product_create_error',
|
||||||
} as WPError;
|
} 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 );
|
onPublishError( wpError );
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ export function useSaveDraft( {
|
||||||
[ productId ]
|
[ productId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isValidating, validate } = useValidations();
|
const { isValidating, validate } = useValidations< Product >();
|
||||||
|
|
||||||
const ariaDisabled =
|
const ariaDisabled =
|
||||||
disabled ||
|
disabled ||
|
||||||
|
@ -76,7 +76,7 @@ export function useSaveDraft( {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await validate();
|
await validate( { status: 'draft' } );
|
||||||
|
|
||||||
await editEntityRecord( 'postType', 'product', productId, {
|
await editEntityRecord( 'postType', 'product', productId, {
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
|
|
|
@ -15,6 +15,21 @@ $table-row-height: calc($grid-unit * 9);
|
||||||
border-bottom: 1px solid $gray-200;
|
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 {
|
&__table {
|
||||||
height: $table-row-height * 5;
|
height: $table-row-height * 5;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -26,6 +41,7 @@ $table-row-height: calc($grid-unit * 9);
|
||||||
border-color: $gray-600;
|
border-color: $gray-600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
margin-left: $gap-smallest;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__filters {
|
&__filters {
|
||||||
|
@ -84,6 +100,7 @@ $table-row-height: calc($grid-unit * 9);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: $gap-smaller;
|
gap: $gap-smaller;
|
||||||
|
margin-right: $gap-smallest;
|
||||||
|
|
||||||
&--delete {
|
&--delete {
|
||||||
&.components-button.components-menu-item__button.is-link {
|
&.components-button.components-menu-item__button.is-link {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { __, sprintf } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CheckboxControl,
|
CheckboxControl,
|
||||||
|
Notice,
|
||||||
Spinner,
|
Spinner,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
|
@ -27,6 +28,8 @@ import {
|
||||||
createElement,
|
createElement,
|
||||||
useRef,
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
Fragment,
|
||||||
|
forwardRef,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { useSelect, useDispatch } from '@wordpress/data';
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
import classnames from 'classnames';
|
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' );
|
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 [ currentPage, setCurrentPage ] = useState( 1 );
|
||||||
const lastVariations = useRef< ProductVariation[] | null >( null );
|
const lastVariations = useRef< ProductVariation[] | null >( null );
|
||||||
const [ perPage, setPerPage ] = useState(
|
const [ perPage, setPerPage ] = useState(
|
||||||
|
@ -90,22 +124,9 @@ export function VariationsTable() {
|
||||||
} ),
|
} ),
|
||||||
[ productId ]
|
[ productId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
const context = useContext( CurrencyContext );
|
const context = useContext( CurrencyContext );
|
||||||
const { formatAmount } = context;
|
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(
|
const { isLoading, latestVariations, isGeneratingVariations } = useSelect(
|
||||||
( select ) => {
|
( select ) => {
|
||||||
const {
|
const {
|
||||||
|
@ -127,6 +148,21 @@ export function VariationsTable() {
|
||||||
[ currentPage, perPage, productId ]
|
[ currentPage, perPage, productId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { totalCount } = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { getProductVariationsTotalCount } = select(
|
||||||
|
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCount: getProductVariationsTotalCount< number >(
|
||||||
|
totalCountRequestParams
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ productId ]
|
||||||
|
);
|
||||||
|
|
||||||
const paginationProps = usePagination( {
|
const paginationProps = usePagination( {
|
||||||
totalCount,
|
totalCount,
|
||||||
defaultPerPage: DEFAULT_VARIATION_PER_PAGE_OPTION,
|
defaultPerPage: DEFAULT_VARIATION_PER_PAGE_OPTION,
|
||||||
|
@ -179,13 +215,17 @@ export function VariationsTable() {
|
||||||
recordEvent( 'product_variations_delete', {
|
recordEvent( 'product_variations_delete', {
|
||||||
source: TRACKS_SOURCE,
|
source: TRACKS_SOURCE,
|
||||||
} );
|
} );
|
||||||
|
invalidateResolution( 'getProductVariations', [
|
||||||
|
requestParams,
|
||||||
|
] );
|
||||||
} )
|
} )
|
||||||
.finally( () =>
|
.finally( () => {
|
||||||
setIsUpdating( ( prevState ) => ( {
|
setIsUpdating( ( prevState ) => ( {
|
||||||
...prevState,
|
...prevState,
|
||||||
[ variationId ]: false,
|
[ variationId ]: false,
|
||||||
} ) )
|
} ) );
|
||||||
);
|
onVariationTableChange( 'delete' );
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleVariationChange(
|
function handleVariationChange(
|
||||||
|
@ -212,12 +252,13 @@ export function VariationsTable() {
|
||||||
__( 'Failed to save variation.', 'woocommerce' )
|
__( 'Failed to save variation.', 'woocommerce' )
|
||||||
);
|
);
|
||||||
} )
|
} )
|
||||||
.finally( () =>
|
.finally( () => {
|
||||||
setIsUpdating( ( prevState ) => ( {
|
setIsUpdating( ( prevState ) => ( {
|
||||||
...prevState,
|
...prevState,
|
||||||
[ variationId ]: false,
|
[ variationId ]: false,
|
||||||
} ) )
|
} ) );
|
||||||
);
|
onVariationTableChange( 'update', [ variation ] );
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateAll( update: Partial< ProductVariation >[] ) {
|
function handleUpdateAll( update: Partial< ProductVariation >[] ) {
|
||||||
|
@ -238,6 +279,7 @@ export function VariationsTable() {
|
||||||
response.update.length
|
response.update.length
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
onVariationTableChange( 'update', update );
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
createErrorNotice(
|
createErrorNotice(
|
||||||
|
@ -266,6 +308,7 @@ export function VariationsTable() {
|
||||||
response.delete.length
|
response.delete.length
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
onVariationTableChange( 'delete' );
|
||||||
} )
|
} )
|
||||||
.catch( () => {
|
.catch( () => {
|
||||||
createErrorNotice(
|
createErrorNotice(
|
||||||
|
@ -275,7 +318,7 @@ export function VariationsTable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-product-variations">
|
<div className="woocommerce-product-variations" ref={ ref }>
|
||||||
{ ( isLoading || isGeneratingVariations ) && (
|
{ ( isLoading || isGeneratingVariations ) && (
|
||||||
<div className="woocommerce-product-variations__loading">
|
<div className="woocommerce-product-variations__loading">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -286,6 +329,21 @@ export function VariationsTable() {
|
||||||
) }
|
) }
|
||||||
</div>
|
</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__header">
|
||||||
<div className="woocommerce-product-variations__selection">
|
<div className="woocommerce-product-variations__selection">
|
||||||
<CheckboxControl
|
<CheckboxControl
|
||||||
|
@ -401,18 +459,25 @@ export function VariationsTable() {
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
|
{ variation.regular_price && (
|
||||||
|
<>
|
||||||
<span
|
<span
|
||||||
className={ classnames(
|
className={ classnames(
|
||||||
'woocommerce-product-variations__status-dot',
|
'woocommerce-product-variations__status-dot',
|
||||||
getProductStockStatusClass( variation )
|
getProductStockStatusClass(
|
||||||
|
variation
|
||||||
|
)
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
●
|
●
|
||||||
</span>
|
</span>
|
||||||
{ getProductStockStatus( variation ) }
|
{ getProductStockStatus( variation ) }
|
||||||
|
</>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
<div className="woocommerce-product-variations__actions">
|
<div className="woocommerce-product-variations__actions">
|
||||||
{ variation.status === 'private' && (
|
{ ( variation.status === 'private' ||
|
||||||
|
! variation.regular_price ) && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
// @ts-expect-error className is missing in TS, should remove this when it is included.
|
// @ts-expect-error className is missing in TS, should remove this when it is included.
|
||||||
className="woocommerce-attribute-list-item__actions-tooltip"
|
className="woocommerce-attribute-list-item__actions-tooltip"
|
||||||
|
@ -459,4 +524,4 @@ export function VariationsTable() {
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} );
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
export type ValidatorResponse = Promise< ValidationError >;
|
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 > = {
|
export type ValidationContextProps< T > = {
|
||||||
errors: ValidationErrors;
|
errors: ValidationErrors;
|
||||||
|
@ -9,7 +12,7 @@ export type ValidationContextProps< T > = {
|
||||||
validator: Validator< T >
|
validator: Validator< T >
|
||||||
): React.Ref< HTMLElement >;
|
): React.Ref< HTMLElement >;
|
||||||
validateField( name: string ): ValidatorResponse;
|
validateField( name: string ): ValidatorResponse;
|
||||||
validateAll(): Promise< ValidationErrors >;
|
validateAll( newData?: Partial< T > ): Promise< ValidationErrors >;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ValidationProviderProps< T > = {
|
export type ValidationProviderProps< T > = {
|
||||||
|
@ -19,9 +22,9 @@ export type ValidationProviderProps< T > = {
|
||||||
export type ValidationError = string | undefined;
|
export type ValidationError = string | undefined;
|
||||||
export type ValidationErrors = Record< string, ValidationError >;
|
export type ValidationErrors = Record< string, ValidationError >;
|
||||||
|
|
||||||
export type ValidatorRegistration = {
|
export type ValidatorRegistration< T > = {
|
||||||
name: string;
|
name: string;
|
||||||
ref: React.Ref< HTMLElement >;
|
ref: React.Ref< HTMLElement >;
|
||||||
error?: ValidationError;
|
error?: ValidationError;
|
||||||
validate(): ValidatorResponse;
|
validate( newData?: Partial< T > ): ValidatorResponse;
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,17 +13,17 @@ function isInvalid( errors: ValidationErrors ) {
|
||||||
return Object.values( errors ).some( Boolean );
|
return Object.values( errors ).some( Boolean );
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useValidations() {
|
export function useValidations< T = unknown >() {
|
||||||
const context = useContext( ValidationContext );
|
const context = useContext( ValidationContext );
|
||||||
const [ isValidating, setIsValidating ] = useState( false );
|
const [ isValidating, setIsValidating ] = useState( false );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValidating,
|
isValidating,
|
||||||
async validate() {
|
async validate( newData?: Partial< T > ) {
|
||||||
setIsValidating( true );
|
setIsValidating( true );
|
||||||
return new Promise< void >( ( resolve, reject ) => {
|
return new Promise< void >( ( resolve, reject ) => {
|
||||||
context
|
context
|
||||||
.validateAll()
|
.validateAll( newData )
|
||||||
.then( ( errors ) => {
|
.then( ( errors ) => {
|
||||||
if ( isInvalid( errors ) ) {
|
if ( isInvalid( errors ) ) {
|
||||||
reject( 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;
|
const validators = validatorsRef.current;
|
||||||
if ( validatorId in validators ) {
|
if ( validatorId in validators ) {
|
||||||
const validator = validators[ validatorId ];
|
const validator = validators[ validatorId ];
|
||||||
const result = validator( initialValue );
|
const result = validator( initialValue, newData );
|
||||||
|
|
||||||
return result.then( ( error ) => {
|
return result.then( ( error ) => {
|
||||||
setErrors( ( currentErrors ) => ( {
|
setErrors( ( currentErrors ) => ( {
|
||||||
|
@ -56,12 +59,17 @@ export function ValidationProvider< T >( {
|
||||||
return Promise.resolve( undefined );
|
return Promise.resolve( undefined );
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateAll(): Promise< ValidationErrors > {
|
async function validateAll(
|
||||||
|
newData: Partial< T >
|
||||||
|
): Promise< ValidationErrors > {
|
||||||
const newErrors: ValidationErrors = {};
|
const newErrors: ValidationErrors = {};
|
||||||
const validators = validatorsRef.current;
|
const validators = validatorsRef.current;
|
||||||
|
|
||||||
for ( const validatorId in validators ) {
|
for ( const validatorId in validators ) {
|
||||||
newErrors[ validatorId ] = await validateField( validatorId );
|
newErrors[ validatorId ] = await validateField(
|
||||||
|
validatorId,
|
||||||
|
newData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors( newErrors );
|
setErrors( newErrors );
|
||||||
|
|
|
@ -33,5 +33,8 @@ export function useConfirmUnsavedProductChanges() {
|
||||||
[ productId ]
|
[ productId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
useConfirmUnsavedChanges( hasEdits || isSaving, preventLeavingProductForm );
|
useConfirmUnsavedChanges(
|
||||||
|
hasEdits || isSaving,
|
||||||
|
preventLeavingProductForm( productId )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useDispatch } from '@wordpress/data';
|
import { resolveSelect, useDispatch } from '@wordpress/data';
|
||||||
import { useEntityProp } from '@wordpress/core-data';
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
import { useCallback, useState } from '@wordpress/element';
|
import { useCallback, useState } from '@wordpress/element';
|
||||||
|
import { getNewPath, getPath, navigateTo } from '@woocommerce/navigation';
|
||||||
import {
|
import {
|
||||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||||
|
Product,
|
||||||
ProductDefaultAttribute,
|
ProductDefaultAttribute,
|
||||||
} from '@woocommerce/data';
|
} from '@woocommerce/data';
|
||||||
|
|
||||||
|
@ -34,6 +36,13 @@ export function useProductVariationsHelper() {
|
||||||
) => {
|
) => {
|
||||||
setIsGenerating( true );
|
setIsGenerating( true );
|
||||||
|
|
||||||
|
const lastStatus = (
|
||||||
|
( await resolveSelect( 'core' ).getEditedEntityRecord(
|
||||||
|
'postType',
|
||||||
|
'product',
|
||||||
|
productId
|
||||||
|
) ) as Product
|
||||||
|
).status;
|
||||||
const hasVariableAttribute = attributes.some(
|
const hasVariableAttribute = attributes.some(
|
||||||
( attr ) => attr.variation
|
( attr ) => attr.variation
|
||||||
);
|
);
|
||||||
|
@ -64,6 +73,13 @@ export function useProductVariationsHelper() {
|
||||||
} )
|
} )
|
||||||
.finally( () => {
|
.finally( () => {
|
||||||
setIsGenerating( false );
|
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';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
export type WPErrorCode =
|
export type WPErrorCode =
|
||||||
|
| 'variable_product_no_variation_prices'
|
||||||
| 'product_invalid_sku'
|
| 'product_invalid_sku'
|
||||||
| 'product_create_error'
|
| 'product_create_error'
|
||||||
| 'product_publish_error'
|
| 'product_publish_error'
|
||||||
|
@ -19,6 +20,8 @@ export type WPError = {
|
||||||
|
|
||||||
export function getProductErrorMessage( error: WPError ) {
|
export function getProductErrorMessage( error: WPError ) {
|
||||||
switch ( error.code ) {
|
switch ( error.code ) {
|
||||||
|
case 'variable_product_no_variation_prices':
|
||||||
|
return error.message;
|
||||||
case 'product_invalid_sku':
|
case 'product_invalid_sku':
|
||||||
return __( 'Invalid or duplicated SKU.', 'woocommerce' );
|
return __( 'Invalid or duplicated SKU.', 'woocommerce' );
|
||||||
case 'product_create_error':
|
case 'product_create_error':
|
||||||
|
|
|
@ -6,10 +6,19 @@ import { Location } from 'react-router-dom';
|
||||||
/**
|
/**
|
||||||
* Allow switching between tabs without prompting for unsaved changes.
|
* Allow switching between tabs without prompting for unsaved changes.
|
||||||
*/
|
*/
|
||||||
export const preventLeavingProductForm = ( toUrl: URL, fromUrl: Location ) => {
|
export const preventLeavingProductForm =
|
||||||
|
( productId?: number ) => ( toUrl: URL, fromUrl: Location ) => {
|
||||||
const toParams = new URLSearchParams( toUrl.search );
|
const toParams = new URLSearchParams( toUrl.search );
|
||||||
const fromParams = new URLSearchParams( fromUrl.search );
|
const fromParams = new URLSearchParams( fromUrl.search );
|
||||||
toParams.delete( 'tab' );
|
toParams.delete( 'tab' );
|
||||||
fromParams.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();
|
return toParams.toString() !== fromParams.toString();
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe( 'preventLeavingProductForm', () => {
|
||||||
const fromUrl = {
|
const fromUrl = {
|
||||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=general',
|
search: 'admin.php?page=wc-admin&path=/product/123&tab=general',
|
||||||
} as Location;
|
} as Location;
|
||||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||||
expect( shouldPrevent ).toBe( true );
|
expect( shouldPrevent ).toBe( true );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ describe( 'preventLeavingProductForm', () => {
|
||||||
const fromUrl = {
|
const fromUrl = {
|
||||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=general',
|
search: 'admin.php?page=wc-admin&path=/product/123&tab=general',
|
||||||
} as Location;
|
} as Location;
|
||||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||||
expect( shouldPrevent ).toBe( true );
|
expect( shouldPrevent ).toBe( true );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -38,7 +38,35 @@ describe( 'preventLeavingProductForm', () => {
|
||||||
const fromUrl = {
|
const fromUrl = {
|
||||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=shipping',
|
search: 'admin.php?page=wc-admin&path=/product/123&tab=shipping',
|
||||||
} as Location;
|
} 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 );
|
expect( shouldPrevent ).toBe( true );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -49,7 +77,7 @@ describe( 'preventLeavingProductForm', () => {
|
||||||
const fromUrl = {
|
const fromUrl = {
|
||||||
search: 'admin.php?page=wc-admin&path=/product/123&tab=shipping&other_param=b',
|
search: 'admin.php?page=wc-admin&path=/product/123&tab=shipping&other_param=b',
|
||||||
} as Location;
|
} as Location;
|
||||||
const shouldPrevent = preventLeavingProductForm( toUrl, fromUrl );
|
const shouldPrevent = preventLeavingProductForm()( toUrl, fromUrl );
|
||||||
expect( shouldPrevent ).toBe( true );
|
expect( shouldPrevent ).toBe( true );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1492,11 +1492,15 @@ export const CoreProfilerController = ( {
|
||||||
hasJetpackSelected: ( context ) => {
|
hasJetpackSelected: ( context ) => {
|
||||||
return (
|
return (
|
||||||
context.pluginsSelected.find(
|
context.pluginsSelected.find(
|
||||||
( plugin ) => plugin === 'jetpack'
|
( plugin ) =>
|
||||||
|
plugin === 'jetpack' ||
|
||||||
|
plugin === 'jetpack-boost'
|
||||||
) !== undefined ||
|
) !== undefined ||
|
||||||
context.pluginsAvailable.find(
|
context.pluginsAvailable.find(
|
||||||
( plugin: Extension ) =>
|
( plugin: Extension ) =>
|
||||||
plugin.key === 'jetpack' && plugin.is_activated
|
( plugin.key === 'jetpack' ||
|
||||||
|
plugin.key === 'jetpack-boost' ) &&
|
||||||
|
plugin.is_activated
|
||||||
) !== undefined
|
) !== undefined
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose';
|
import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose';
|
||||||
import { useMemo, useContext } from '@wordpress/element';
|
import { useContext } from '@wordpress/element';
|
||||||
import { Disabled } from '@wordpress/components';
|
import { Disabled } from '@wordpress/components';
|
||||||
import {
|
import {
|
||||||
__unstableEditorStyles as EditorStyles,
|
__unstableEditorStyles as EditorStyles,
|
||||||
|
@ -52,7 +52,6 @@ export type ScaledBlockPreviewProps = {
|
||||||
function ScaledBlockPreview( {
|
function ScaledBlockPreview( {
|
||||||
viewportWidth,
|
viewportWidth,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
minHeight,
|
|
||||||
settings,
|
settings,
|
||||||
additionalStyles,
|
additionalStyles,
|
||||||
onClickNavigationItem,
|
onClickNavigationItem,
|
||||||
|
@ -70,28 +69,14 @@ function ScaledBlockPreview( {
|
||||||
viewportWidth = containerWidth;
|
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.
|
// Initialize on render instead of module top level, to avoid circular dependency issues.
|
||||||
MemoizedBlockList = MemoizedBlockList || pure( BlockList );
|
MemoizedBlockList = MemoizedBlockList || pure( BlockList );
|
||||||
const scale = containerWidth / viewportWidth;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledProvider value={ true }>
|
<DisabledProvider value={ true }>
|
||||||
<Iframe
|
<Iframe
|
||||||
|
aria-hidden
|
||||||
|
tabIndex={ -1 }
|
||||||
contentRef={ useRefEffect( ( bodyElement: HTMLBodyElement ) => {
|
contentRef={ useRefEffect( ( bodyElement: HTMLBodyElement ) => {
|
||||||
const {
|
const {
|
||||||
ownerDocument: { documentElement },
|
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>
|
<style>
|
||||||
{ `
|
{ `
|
||||||
.block-editor-block-list__block::before,
|
.block-editor-block-list__block::before,
|
||||||
|
@ -267,7 +242,6 @@ function ScaledBlockPreview( {
|
||||||
${ additionalStyles }
|
${ additionalStyles }
|
||||||
` }
|
` }
|
||||||
</style>
|
</style>
|
||||||
{ contentResizeListener }
|
|
||||||
<MemoizedBlockList renderAppender={ false } />
|
<MemoizedBlockList renderAppender={ false } />
|
||||||
{ /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ }
|
{ /* Only load font families when there are two font families (font-paring selection). Otherwise, it is not needed. */ }
|
||||||
{ externalFontFamilies.length === 2 && (
|
{ externalFontFamilies.length === 2 && (
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import classNames from 'classnames';
|
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
import { useEntityRecords } from '@wordpress/core-data';
|
import { useEntityRecords } from '@wordpress/core-data';
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
|
@ -46,7 +45,7 @@ export const BlockEditor = ( {} ) => {
|
||||||
: 'topDown';
|
: 'topDown';
|
||||||
|
|
||||||
const previewOpacity = useScrollOpacity(
|
const previewOpacity = useScrollOpacity(
|
||||||
'.interface-navigable-region.interface-interface-skeleton__content',
|
'.woocommerce-customize-store__block-editor iframe',
|
||||||
scrollDirection
|
scrollDirection
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -92,59 +91,6 @@ export const BlockEditor = ( {} ) => {
|
||||||
[ history, urlParams, pages ]
|
[ 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 (
|
return (
|
||||||
<div className="woocommerce-customize-store__block-editor">
|
<div className="woocommerce-customize-store__block-editor">
|
||||||
<div className={ 'woocommerce-block-preview-container' }>
|
<div className={ 'woocommerce-block-preview-container' }>
|
||||||
|
|
|
@ -24,14 +24,14 @@ export const useEditorScroll = ( {
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewContainer =
|
const previewContainer =
|
||||||
document.querySelector< HTMLDivElement >( editorSelector );
|
document.querySelector< HTMLIFrameElement >( editorSelector );
|
||||||
if ( previewContainer ) {
|
if ( previewContainer ) {
|
||||||
previewContainer?.scrollTo(
|
previewContainer.contentWindow?.scrollTo(
|
||||||
0,
|
0,
|
||||||
scrollDirection === 'bottom'
|
scrollDirection === 'bottom'
|
||||||
? previewContainer?.scrollHeight
|
? previewContainer.contentDocument?.body.scrollHeight || 0
|
||||||
: 0
|
: 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [ isEditorLoading, scrollDirection ] );
|
}, [ isEditorLoading, editorSelector, scrollDirection ] );
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
/* 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.
|
||||||
|
export 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',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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 TEMPLATES = {
|
||||||
|
template1: LARGE_BUSINESS_TEMPLATES.template1,
|
||||||
|
template2: LARGE_BUSINESS_TEMPLATES.template2,
|
||||||
|
template3: LARGE_BUSINESS_TEMPLATES.template3,
|
||||||
|
template4: SMALL_MEDIUM_BUSINESS_TEMPLATES.template1,
|
||||||
|
template5: SMALL_MEDIUM_BUSINESS_TEMPLATES.template2,
|
||||||
|
template6: SMALL_MEDIUM_BUSINESS_TEMPLATES.template3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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 patternsToNameMap = ( blockPatterns: Pattern[] ) =>
|
||||||
|
blockPatterns.reduce(
|
||||||
|
( acc: Record< string, Pattern >, pattern: Pattern ) => {
|
||||||
|
acc[ pattern.name ] = pattern;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useHomeTemplates = () => {
|
||||||
|
const { blockPatterns, isLoading } = usePatterns();
|
||||||
|
|
||||||
|
const patternsByName = useMemo(
|
||||||
|
() => patternsToNameMap( blockPatterns ),
|
||||||
|
[ blockPatterns ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const homeTemplates = useMemo( () => {
|
||||||
|
if ( isLoading ) return {};
|
||||||
|
const recommendedTemplates = 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 { useMemo } from '@wordpress/element';
|
||||||
import { BlockInstance, parse } from '@wordpress/blocks';
|
import { BlockInstance, parse } from '@wordpress/blocks';
|
||||||
|
|
||||||
type Pattern = {
|
export type Pattern = {
|
||||||
blockTypes: string[];
|
blockTypes: string[];
|
||||||
categories: string[];
|
categories: string[];
|
||||||
content: string;
|
content: string;
|
||||||
|
@ -18,15 +18,17 @@ type Pattern = {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PatternWithBlocks = Pattern & {
|
export type PatternWithBlocks = Pattern & {
|
||||||
blocks: BlockInstance[];
|
blocks: BlockInstance[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePatternsByCategory = ( category: string ) => {
|
export const usePatterns = () => {
|
||||||
const { blockPatterns, isLoading } = useSelect(
|
const { blockPatterns, isLoading } = useSelect(
|
||||||
( select ) => ( {
|
( select ) => ( {
|
||||||
|
blockPatterns: select(
|
||||||
|
coreStore
|
||||||
// @ts-ignore - This is valid.
|
// @ts-ignore - This is valid.
|
||||||
blockPatterns: select( coreStore ).getBlockPatterns(),
|
).getBlockPatterns() as Pattern[],
|
||||||
isLoading:
|
isLoading:
|
||||||
// @ts-ignore - This is valid.
|
// @ts-ignore - This is valid.
|
||||||
! select( coreStore ).hasFinishedResolution(
|
! 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( () => {
|
const patternsByCategory: PatternWithBlocks[] = useMemo( () => {
|
||||||
return ( blockPatterns || [] )
|
return ( blockPatterns || [] )
|
||||||
.filter( ( pattern: Pattern ) =>
|
.filter( ( pattern: Pattern ) =>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
/* eslint-disable @woocommerce/dependency-group */
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState } from '@wordpress/element';
|
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';
|
type ScrollDirection = 'topDown' | 'bottomUp';
|
||||||
|
|
||||||
|
@ -11,21 +15,37 @@ export const useScrollOpacity = (
|
||||||
sensitivity = 0.2
|
sensitivity = 0.2
|
||||||
) => {
|
) => {
|
||||||
const [ opacity, setOpacity ] = useState( 0.05 );
|
const [ opacity, setOpacity ] = useState( 0.05 );
|
||||||
|
const isEditorLoading = useIsSiteEditorLoading();
|
||||||
|
|
||||||
useEffect( () => {
|
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 = () => {
|
const handleScroll = () => {
|
||||||
if ( targetElement ) {
|
if ( ! targetElement ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentElement = isIFrame
|
||||||
|
? ( targetElement as Document ).documentElement
|
||||||
|
: ( targetElement as Element );
|
||||||
|
|
||||||
const maxScrollHeight =
|
const maxScrollHeight =
|
||||||
targetElement.scrollHeight - targetElement.clientHeight;
|
contentElement.scrollHeight - contentElement.clientHeight;
|
||||||
const currentScrollPosition = targetElement.scrollTop;
|
const currentScrollPosition = contentElement.scrollTop;
|
||||||
const maxEffectScroll = maxScrollHeight * sensitivity;
|
const maxEffectScroll = maxScrollHeight * sensitivity;
|
||||||
|
|
||||||
let calculatedOpacity;
|
let calculatedOpacity;
|
||||||
if ( direction === 'bottomUp' ) {
|
if ( direction === 'bottomUp' ) {
|
||||||
calculatedOpacity =
|
calculatedOpacity =
|
||||||
1 - currentScrollPosition / maxEffectScroll;
|
maxScrollHeight / maxEffectScroll -
|
||||||
|
currentScrollPosition / maxEffectScroll;
|
||||||
} else {
|
} else {
|
||||||
calculatedOpacity = currentScrollPosition / maxEffectScroll;
|
calculatedOpacity = currentScrollPosition / maxEffectScroll;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +59,6 @@ export const useScrollOpacity = (
|
||||||
);
|
);
|
||||||
|
|
||||||
setOpacity( calculatedOpacity );
|
setOpacity( calculatedOpacity );
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( targetElement ) {
|
if ( targetElement ) {
|
||||||
|
@ -51,7 +70,7 @@ export const useScrollOpacity = (
|
||||||
targetElement.removeEventListener( 'scroll', handleScroll );
|
targetElement.removeEventListener( 'scroll', handleScroll );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [ selector, direction, sensitivity ] );
|
}, [ selector, direction, sensitivity, isEditorLoading ] );
|
||||||
|
|
||||||
return opacity;
|
return opacity;
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import { TourKit, TourKitTypes } from '@woocommerce/components';
|
import { TourKit, TourKitTypes } from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
export * from './use-onboarding-tour';
|
export * from './use-onboarding-tour';
|
||||||
|
|
||||||
type OnboardingTourProps = {
|
type OnboardingTourProps = {
|
||||||
|
@ -83,8 +84,15 @@ export const OnboardingTour = ( {
|
||||||
],
|
],
|
||||||
closeHandler: ( _steps, _currentStepIndex, source ) => {
|
closeHandler: ( _steps, _currentStepIndex, source ) => {
|
||||||
if ( source === 'done-btn' ) {
|
if ( source === 'done-btn' ) {
|
||||||
|
// Click on "Take a tour" button
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_tour_start'
|
||||||
|
);
|
||||||
setShowWelcomeTour( false );
|
setShowWelcomeTour( false );
|
||||||
} else {
|
} else {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_tour_skip'
|
||||||
|
);
|
||||||
onClose();
|
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>
|
></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'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -180,13 +180,12 @@ function ResizableFrame( {
|
||||||
left: 0,
|
left: 0,
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 0.6,
|
||||||
left: -10,
|
left: -10,
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
left: -10,
|
left: -10,
|
||||||
scaleY: 1.3,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const currentResizeHandleVariant = ( () => {
|
const currentResizeHandleVariant = ( () => {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,7 +21,8 @@ export const ColorPalette = () => {
|
||||||
gap={ 4 }
|
gap={ 4 }
|
||||||
className="woocommerce-customize-store_color-palette-container"
|
className="woocommerce-customize-store_color-palette-container"
|
||||||
>
|
>
|
||||||
{ COLOR_PALETTES.map( ( variation, index ) => (
|
{ /* TODO: Show 9 colors based on the AI recommendation */ }
|
||||||
|
{ COLOR_PALETTES.slice( 0, 9 ).map( ( variation, index ) => (
|
||||||
<VariationContainer key={ index } variation={ variation }>
|
<VariationContainer key={ index } variation={ variation }>
|
||||||
<ColorPaletteVariationPreview title={ variation?.title } />
|
<ColorPaletteVariationPreview title={ variation?.title } />
|
||||||
</VariationContainer>
|
</VariationContainer>
|
||||||
|
|
|
@ -4,15 +4,19 @@
|
||||||
*/
|
*/
|
||||||
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
|
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
|
||||||
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
||||||
|
import { useContext } from '@wordpress/element';
|
||||||
|
import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
useGlobalStyle,
|
useGlobalStyle,
|
||||||
useGlobalSetting,
|
useGlobalSetting,
|
||||||
useSettingsForBlockElement,
|
useSettingsForBlockElement,
|
||||||
ColorPanel: StylesColorPanel,
|
ColorPanel: StylesColorPanel,
|
||||||
|
GlobalStylesContext,
|
||||||
} = unlock( blockEditorPrivateApis );
|
} = unlock( blockEditorPrivateApis );
|
||||||
|
|
||||||
export const ColorPanel = () => {
|
export const ColorPanel = () => {
|
||||||
|
const { setUserConfig } = useContext( GlobalStylesContext );
|
||||||
const [ style ] = useGlobalStyle( '', undefined, 'user', {
|
const [ style ] = useGlobalStyle( '', undefined, 'user', {
|
||||||
shouldDecodeEncode: false,
|
shouldDecodeEncode: false,
|
||||||
} );
|
} );
|
||||||
|
@ -22,11 +26,25 @@ export const ColorPanel = () => {
|
||||||
const [ rawSettings ] = useGlobalSetting( '' );
|
const [ rawSettings ] = useGlobalSetting( '' );
|
||||||
const settings = useSettingsForBlockElement( rawSettings );
|
const settings = useSettingsForBlockElement( rawSettings );
|
||||||
|
|
||||||
|
const onChange = ( ...props ) => {
|
||||||
|
setStyle( ...props );
|
||||||
|
setUserConfig( ( currentConfig ) => ( {
|
||||||
|
...currentConfig,
|
||||||
|
settings: mergeBaseAndUserConfigs( currentConfig.settings, {
|
||||||
|
color: {
|
||||||
|
palette: {
|
||||||
|
hasCreatedOwnColors: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} ),
|
||||||
|
} ) );
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StylesColorPanel
|
<StylesColorPanel
|
||||||
inheritedValue={ inheritedStyle }
|
inheritedValue={ inheritedStyle }
|
||||||
value={ style }
|
value={ style }
|
||||||
onChange={ setStyle }
|
onChange={ onChange }
|
||||||
settings={ settings }
|
settings={ settings }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,6 +28,15 @@ export const VariationContainer = ( { variation, children } ) => {
|
||||||
}, [ variation, base ] );
|
}, [ variation, base ] );
|
||||||
|
|
||||||
const selectVariation = () => {
|
const selectVariation = () => {
|
||||||
|
// Remove the hasCreatedOwnColors flag if the user is switching to a color palette
|
||||||
|
if (
|
||||||
|
variation.settings.color &&
|
||||||
|
user.settings.color &&
|
||||||
|
user.settings.color.hasCreatedOwnColors
|
||||||
|
) {
|
||||||
|
delete user.settings.color.palette.hasCreatedOwnColors;
|
||||||
|
}
|
||||||
|
|
||||||
setUserConfig( () => {
|
setUserConfig( () => {
|
||||||
return {
|
return {
|
||||||
settings: mergeBaseAndUserConfigs(
|
settings: mergeBaseAndUserConfigs(
|
||||||
|
@ -48,7 +57,6 @@ export const VariationContainer = ( { variation, children } ) => {
|
||||||
selectVariation();
|
selectVariation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActive = useMemo( () => {
|
const isActive = useMemo( () => {
|
||||||
if ( variation.settings.color ) {
|
if ( variation.settings.color ) {
|
||||||
return isEqual( variation.settings.color, user.settings.color );
|
return isEqual( variation.settings.color, user.settings.color );
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { SidebarNavigationScreenTypography } from './sidebar-navigation-screen-t
|
||||||
import { SidebarNavigationScreenHeader } from './sidebar-navigation-screen-header';
|
import { SidebarNavigationScreenHeader } from './sidebar-navigation-screen-header';
|
||||||
import { SidebarNavigationScreenHomepage } from './sidebar-navigation-screen-homepage';
|
import { SidebarNavigationScreenHomepage } from './sidebar-navigation-screen-homepage';
|
||||||
import { SidebarNavigationScreenFooter } from './sidebar-navigation-screen-footer';
|
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 { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo';
|
||||||
|
|
||||||
import { SaveHub } from './save-hub';
|
import { SaveHub } from './save-hub';
|
||||||
|
@ -124,9 +124,10 @@ function SidebarScreens() {
|
||||||
<NavigatorScreen path="/customize-store/assembler-hub/footer">
|
<NavigatorScreen path="/customize-store/assembler-hub/footer">
|
||||||
<SidebarNavigationScreenFooter />
|
<SidebarNavigationScreenFooter />
|
||||||
</NavigatorScreen>
|
</NavigatorScreen>
|
||||||
<NavigatorScreen path="/customize-store/assembler-hub/pages">
|
{ /* TODO: Implement pages sidebar in Phrase 2 */ }
|
||||||
|
{ /* <NavigatorScreen path="/customize-store/assembler-hub/pages">
|
||||||
<SidebarNavigationScreenPages />
|
<SidebarNavigationScreenPages />
|
||||||
</NavigatorScreen>
|
</NavigatorScreen> */ }
|
||||||
<NavigatorScreen path="/customize-store/assembler-hub/logo">
|
<NavigatorScreen path="/customize-store/assembler-hub/logo">
|
||||||
<SidebarNavigationScreenLogo />
|
<SidebarNavigationScreenLogo />
|
||||||
</NavigatorScreen>
|
</NavigatorScreen>
|
||||||
|
@ -146,8 +147,8 @@ function Sidebar() {
|
||||||
initialPath={ initialPath.current }
|
initialPath={ initialPath.current }
|
||||||
>
|
>
|
||||||
<SidebarScreens />
|
<SidebarScreens />
|
||||||
</NavigatorProvider>
|
|
||||||
<SaveHub />
|
<SaveHub />
|
||||||
|
</NavigatorProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
import {
|
import {
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
__experimentalHStack as HStack,
|
__experimentalHStack as HStack,
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
__experimentalUseNavigator as useNavigator,
|
||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
|
@ -22,6 +24,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
|
||||||
import { store as noticesStore } from '@wordpress/notices';
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
|
import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -40,13 +43,13 @@ export const SaveHub = () => {
|
||||||
const urlParams = useQuery();
|
const urlParams = useQuery();
|
||||||
const { sendEvent } = useContext( CustomizeStoreContext );
|
const { sendEvent } = useContext( CustomizeStoreContext );
|
||||||
const [ isResolving, setIsResolving ] = useState< boolean >( false );
|
const [ isResolving, setIsResolving ] = useState< boolean >( false );
|
||||||
|
const navigator = useNavigator();
|
||||||
|
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
const { __unstableMarkLastChangeAsPersistent } =
|
const { __unstableMarkLastChangeAsPersistent } =
|
||||||
useDispatch( blockEditorStore );
|
useDispatch( blockEditorStore );
|
||||||
|
|
||||||
const { createSuccessNotice, createErrorNotice, removeNotice } =
|
const { createErrorNotice, removeNotice } = useDispatch( noticesStore );
|
||||||
useDispatch( noticesStore );
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dirtyEntityRecords,
|
dirtyEntityRecords,
|
||||||
|
@ -142,6 +145,13 @@ export const SaveHub = () => {
|
||||||
}, [ urlParams.path ] );
|
}, [ urlParams.path ] );
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
|
const source = `${ urlParams.path.replace(
|
||||||
|
'/customize-store/assembler-hub/',
|
||||||
|
''
|
||||||
|
) }`;
|
||||||
|
recordEvent( 'customize_your_store_assembler_hub_save_click', {
|
||||||
|
source,
|
||||||
|
} );
|
||||||
removeNotice( saveNoticeId );
|
removeNotice( saveNoticeId );
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -168,10 +178,7 @@ export const SaveHub = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createSuccessNotice( __( 'Site updated.', 'woocommerce' ), {
|
navigator.goToParent();
|
||||||
type: 'snackbar',
|
|
||||||
id: saveNoticeId,
|
|
||||||
} );
|
|
||||||
} catch ( error ) {
|
} catch ( error ) {
|
||||||
createErrorNotice(
|
createErrorNotice(
|
||||||
`${ __( 'Saving failed.', 'woocommerce' ) } ${ error }`
|
`${ __( 'Saving failed.', 'woocommerce' ) } ${ error }`
|
||||||
|
@ -185,6 +192,10 @@ export const SaveHub = () => {
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_done_click'
|
||||||
|
);
|
||||||
|
|
||||||
setIsResolving( true );
|
setIsResolving( true );
|
||||||
sendEvent( 'FINISH_CUSTOMIZATION' );
|
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.
|
// @ts-ignore No types for this exist yet.
|
||||||
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
|
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
|
||||||
import { PanelBody } from '@wordpress/components';
|
import { PanelBody } from '@wordpress/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -70,14 +71,38 @@ export const SidebarNavigationScreenColorPalette = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_editor_link_click',
|
||||||
|
{
|
||||||
|
source: 'color-palette',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.open(
|
||||||
|
`${ ADMIN_URL }site-editor.php`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} }
|
||||||
|
href=""
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
StyleLink: (
|
StyleLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
|
onClick={ () => {
|
||||||
type="external"
|
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,
|
useMemo,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { Link } from '@woocommerce/components';
|
import { Link } from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { Spinner } from '@wordpress/components';
|
import { Spinner } from '@wordpress/components';
|
||||||
// @ts-expect-error Missing type in core-data.
|
// @ts-expect-error Missing type in core-data.
|
||||||
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
||||||
|
@ -34,8 +35,7 @@ const SUPPORTED_FOOTER_PATTERNS = [
|
||||||
|
|
||||||
export const SidebarNavigationScreenFooter = () => {
|
export const SidebarNavigationScreenFooter = () => {
|
||||||
useEditorScroll( {
|
useEditorScroll( {
|
||||||
editorSelector:
|
editorSelector: '.woocommerce-customize-store__block-editor iframe',
|
||||||
'.interface-navigable-region.interface-interface-skeleton__content',
|
|
||||||
scrollDirection: 'bottom',
|
scrollDirection: 'bottom',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -86,8 +86,20 @@ export const SidebarNavigationScreenFooter = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
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,
|
useMemo,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { Link } from '@woocommerce/components';
|
import { Link } from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { Spinner } from '@wordpress/components';
|
import { Spinner } from '@wordpress/components';
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
import { __experimentalBlockPatternsList as BlockPatternList } from '@wordpress/block-editor';
|
||||||
|
@ -35,8 +36,7 @@ const SUPPORTED_HEADER_PATTERNS = [
|
||||||
|
|
||||||
export const SidebarNavigationScreenHeader = () => {
|
export const SidebarNavigationScreenHeader = () => {
|
||||||
useEditorScroll( {
|
useEditorScroll( {
|
||||||
editorSelector:
|
editorSelector: '.woocommerce-customize-store__block-editor iframe',
|
||||||
'.interface-navigable-region.interface-interface-skeleton__content',
|
|
||||||
scrollDirection: 'top',
|
scrollDirection: 'top',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -85,8 +85,20 @@ export const SidebarNavigationScreenHeader = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
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
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { createInterpolateElement } from '@wordpress/element';
|
import {
|
||||||
|
createInterpolateElement,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { Link } from '@woocommerce/components';
|
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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
|
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
|
||||||
import { ADMIN_URL } from '~/utils/admin-settings';
|
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 = () => {
|
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 (
|
return (
|
||||||
<SidebarNavigationScreen
|
<SidebarNavigationScreen
|
||||||
title={ __( 'Change your homepage', 'woocommerce' ) }
|
title={ __( 'Change your homepage', 'woocommerce' ) }
|
||||||
|
@ -22,16 +67,45 @@ export const SidebarNavigationScreenHomepage = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_editor_link_click',
|
||||||
|
{
|
||||||
|
source: 'homepage',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.open(
|
||||||
|
`${ ADMIN_URL }site-editor.php`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} }
|
||||||
|
href=""
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
) }
|
) }
|
||||||
content={
|
content={
|
||||||
<>
|
<div className="woocommerce-customize-store__sidebar-homepage-content">
|
||||||
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
|
<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,
|
header,
|
||||||
home,
|
home,
|
||||||
footer,
|
footer,
|
||||||
pages,
|
|
||||||
} from '@wordpress/icons';
|
} from '@wordpress/icons';
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
import SidebarNavigationItem from '@wordpress/edit-site/build-module/components/sidebar-navigation-item';
|
import SidebarNavigationItem from '@wordpress/edit-site/build-module/components/sidebar-navigation-item';
|
||||||
import { Link } from '@woocommerce/components';
|
import { Link } from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -45,8 +45,20 @@ export const SidebarNavigationScreenMain = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
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"
|
path="/customize-store/assembler-hub/logo"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ siteLogo }
|
icon={ siteLogo }
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||||
|
{
|
||||||
|
item: 'logo',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ __( 'Add your logo', 'woocommerce' ) }
|
{ __( 'Add your logo', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton>
|
||||||
|
@ -72,6 +92,14 @@ export const SidebarNavigationScreenMain = () => {
|
||||||
path="/customize-store/assembler-hub/color-palette"
|
path="/customize-store/assembler-hub/color-palette"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ color }
|
icon={ color }
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||||
|
{
|
||||||
|
item: 'color-palette',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ __( 'Change the color palette', 'woocommerce' ) }
|
{ __( 'Change the color palette', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton>
|
||||||
|
@ -80,6 +108,14 @@ export const SidebarNavigationScreenMain = () => {
|
||||||
path="/customize-store/assembler-hub/typography"
|
path="/customize-store/assembler-hub/typography"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ typography }
|
icon={ typography }
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||||
|
{
|
||||||
|
item: 'typography',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ __( 'Change fonts', 'woocommerce' ) }
|
{ __( 'Change fonts', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton>
|
||||||
|
@ -95,6 +131,14 @@ export const SidebarNavigationScreenMain = () => {
|
||||||
path="/customize-store/assembler-hub/header"
|
path="/customize-store/assembler-hub/header"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ header }
|
icon={ header }
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||||
|
{
|
||||||
|
item: 'header',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ __( 'Change your header', 'woocommerce' ) }
|
{ __( 'Change your header', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton>
|
||||||
|
@ -103,6 +147,14 @@ export const SidebarNavigationScreenMain = () => {
|
||||||
path="/customize-store/assembler-hub/homepage"
|
path="/customize-store/assembler-hub/homepage"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ home }
|
icon={ home }
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||||
|
{
|
||||||
|
item: 'home',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ __( 'Change your homepage', 'woocommerce' ) }
|
{ __( 'Change your homepage', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton>
|
||||||
|
@ -111,17 +163,26 @@ export const SidebarNavigationScreenMain = () => {
|
||||||
path="/customize-store/assembler-hub/footer"
|
path="/customize-store/assembler-hub/footer"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ footer }
|
icon={ footer }
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_sidebar_item_click',
|
||||||
|
{
|
||||||
|
item: 'footer',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ __( 'Change your footer', 'woocommerce' ) }
|
{ __( 'Change your footer', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton>
|
||||||
<NavigatorButton
|
{ /* TODO: Turn on this in Phrase 2 */ }
|
||||||
|
{ /* <NavigatorButton
|
||||||
as={ SidebarNavigationItem }
|
as={ SidebarNavigationItem }
|
||||||
path="/customize-store/assembler-hub/pages"
|
path="/customize-store/assembler-hub/pages"
|
||||||
withChevron
|
withChevron
|
||||||
icon={ pages }
|
icon={ pages }
|
||||||
>
|
>
|
||||||
{ __( 'Add and edit other pages', 'woocommerce' ) }
|
{ __( 'Add and edit other pages', 'woocommerce' ) }
|
||||||
</NavigatorButton>
|
</NavigatorButton> */ }
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Link } from '@woocommerce/components';
|
import { Link } from '@woocommerce/components';
|
||||||
import { createInterpolateElement } from '@wordpress/element';
|
import { createInterpolateElement } from '@wordpress/element';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -23,8 +24,20 @@ export const SidebarNavigationScreenPages = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_editor_link_click',
|
||||||
|
{
|
||||||
|
source: 'pages',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.open(
|
||||||
|
`${ ADMIN_URL }site-editor.php`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} }
|
||||||
|
href=""
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
PageLink: (
|
PageLink: (
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { noop } from 'lodash';
|
||||||
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
|
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -42,14 +43,38 @@ export const SidebarNavigationScreenTypography = () => {
|
||||||
{
|
{
|
||||||
EditorLink: (
|
EditorLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php` }
|
onClick={ () => {
|
||||||
type="external"
|
recordEvent(
|
||||||
|
'customize_your_store_assembler_hub_editor_link_click',
|
||||||
|
{
|
||||||
|
source: 'typography',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
window.open(
|
||||||
|
`${ ADMIN_URL }site-editor.php`,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} }
|
||||||
|
href=""
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
StyleLink: (
|
StyleLink: (
|
||||||
<Link
|
<Link
|
||||||
href={ `${ ADMIN_URL }site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
|
onClick={ () => {
|
||||||
type="external"
|
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-top: 80px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
width: 348px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-site-sidebar-navigation-screen__title-icon,
|
.edit-site-sidebar-navigation-screen__title-icon,
|
||||||
|
@ -118,10 +119,14 @@
|
||||||
|
|
||||||
.edit-site-layout__sidebar {
|
.edit-site-layout__sidebar {
|
||||||
.edit-site-sidebar__content {
|
.edit-site-sidebar__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.components-navigator-screen {
|
.components-navigator-screen {
|
||||||
will-change: auto;
|
will-change: auto;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,9 +248,13 @@
|
||||||
|
|
||||||
.woocommerce-customize-store__sidebar-logo-container {
|
.woocommerce-customize-store__sidebar-logo-container {
|
||||||
margin: 12px 0 32px;
|
margin: 12px 0 32px;
|
||||||
width: 324px;
|
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 324px;
|
||||||
|
|
||||||
|
.woocommerce-customize-store_custom-logo {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-customize-store__sidebar-logo-content {
|
.woocommerce-customize-store__sidebar-logo-content {
|
||||||
|
@ -411,12 +420,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 */
|
/* Preview Canvas */
|
||||||
.edit-site-layout__canvas {
|
.edit-site-layout__canvas {
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
padding: 0 16px;
|
left: 12px; // the default styles for this undersizes the width by 24px so we want to center this
|
||||||
|
padding: 0 4px 0 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-site-resizable-frame__handle {
|
.edit-site-resizable-frame__handle {
|
||||||
|
@ -427,13 +447,13 @@
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background: #d7defb;
|
background: var(--wp-admin-theme-color-background-25);
|
||||||
left: 5px !important;
|
left: 5px !important;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|
||||||
.components-popover__content {
|
.components-popover__content {
|
||||||
color: #1d35b4;
|
color: var(--wp-admin-theme-color-darker-20);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -452,10 +472,18 @@
|
||||||
.woocommerce-customize-store__block-editor,
|
.woocommerce-customize-store__block-editor,
|
||||||
.edit-site-layout:not(.is-full-canvas) .edit-site-layout__canvas > div .interface-interface-skeleton__content {
|
.edit-site-layout:not(.is-full-canvas) .edit-site-layout__canvas > div .interface-interface-skeleton__content {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
|
||||||
|
.woocommerce-customize-store__block-editor,
|
||||||
|
.woocommerce-block-preview-container,
|
||||||
|
.auto-block-preview__container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interface-interface-skeleton__content {
|
iframe {
|
||||||
@include custom-scrollbars-on-hover(transparent, $gray-600);
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-site-resizable-frame__inner-content {
|
.edit-site-resizable-frame__inner-content {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<svg width="399" height="351" viewBox="0 0 399 351" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M690.229 71.6773C732.419 29.4861 704.871 -66.489 628.68 -142.668C552.49 -218.86 456.531 -246.423 414.327 -204.232C387.767 -177.672 388.854 -129.818 412.279 -78.9995C293.927 -161.631 168.994 -182.204 107.389 -120.612C45.7841 -59.0053 66.3567 65.9162 149 184.286C98.1829 160.846 50.3307 159.773 23.7712 186.333C-18.4189 228.525 9.12891 324.5 85.3197 400.678C161.51 476.871 257.469 504.419 299.659 462.228C326.219 435.668 325.131 387.815 301.707 336.996C420.073 419.628 545.006 440.201 606.597 378.608C668.202 317.002 647.629 192.08 564.986 73.7106C615.803 97.1502 663.655 98.2233 690.215 71.6632L690.229 71.6773Z" fill="#E6DCFF" fill-opacity="0.5"/>
|
||||||
|
<path d="M376.07 217H319.352V229.555H376.07V217Z" fill="#271B3D"/>
|
||||||
|
<path d="M376.07 229.555H319.352V250.479H376.07V229.555Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M367.337 221.185L371.521 225.37" stroke="white" stroke-width="0.71" stroke-miterlimit="10"/>
|
||||||
|
<path d="M371.521 221.185L367.337 225.37" stroke="white" stroke-width="0.71" stroke-miterlimit="10"/>
|
||||||
|
<path d="M334.217 324.551H376.066V303.627H334.217V324.551Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M240.498 324.551H282.346V303.627H240.498V324.551Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M287.36 324.551H329.208V303.627H287.36V324.551Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M308.272 299.132H350.12V278.208H308.272V299.132Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M355.142 299.132H376.066V278.208H355.142V299.132Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M261.694 293.642C274.881 298.601 282.371 290.256 282.371 290.256L248.478 277.509L281.459 262.557C281.459 262.557 273.437 254.722 260.602 260.544C255.735 262.749 246.829 268.457 238.79 273.868L223.499 268.118C224.173 263.636 221.657 259.129 217.251 257.472C212.187 255.568 206.517 258.137 204.613 263.205C202.708 268.269 205.278 273.939 210.346 275.843C214.849 277.538 219.82 275.689 222.21 271.7C223.821 272.696 227.776 275.12 232.672 278.036C227.985 281.263 224.202 283.941 222.653 285.046C220.009 281.225 214.916 279.71 210.538 281.698C205.609 283.933 203.42 289.758 205.655 294.688C207.889 299.618 213.719 301.806 218.644 299.572C222.934 297.626 225.148 292.96 224.177 288.536L239.054 281.79C247.436 286.657 256.697 291.767 261.698 293.646L261.694 293.642ZM212.074 271.236C209.551 270.286 208.266 267.457 209.216 264.934C210.166 262.406 212.995 261.125 215.518 262.075C218.042 263.025 219.327 265.854 218.377 268.378C217.427 270.901 214.598 272.186 212.074 271.236ZM216.611 295.085C214.154 296.199 211.246 295.106 210.132 292.65C209.019 290.193 210.111 287.285 212.568 286.172C215.024 285.059 217.933 286.151 219.046 288.607C220.159 291.064 219.067 293.972 216.611 295.085Z" fill="#271B3D"/>
|
||||||
|
<path d="M314.552 265.804H323.88" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M327.869 265.804H337.192" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M341.181 265.804H350.509" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M354.497 265.804H363.821" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M366.817 265.804H376.145" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M301.24 265.804H310.564" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M287.924 265.804H297.248" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
<path d="M236.378 227.437C236.378 226.429 235.892 225.501 235.072 224.736L228.891 227.462L235.038 230.171C235.875 229.402 236.378 228.457 236.378 227.437Z" fill="#271B3D"/>
|
||||||
|
<path d="M235.072 224.736C235.888 225.501 236.377 226.429 236.377 227.437C236.377 228.458 235.879 229.398 235.038 230.171L251.702 237.92H257.239V217H251.702L235.072 224.736Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M251.798 217C250.053 217.949 248.706 218.693 248.706 220.847C248.706 222.288 249.442 223.502 250.748 224.148C249.451 224.804 248.714 226.03 248.706 227.462C248.714 228.89 249.446 230.117 250.748 230.776C249.442 231.423 248.706 232.637 248.706 234.077C248.706 236.232 250.053 236.975 251.798 237.924H307.704L311.328 232.107V222.817L307.704 217H251.798Z" fill="#271B3D"/>
|
||||||
|
<path d="M307.707 237.924L311.331 232.11V222.818L307.707 217H297.659L294.035 222.818V232.11L297.659 237.924H307.707Z" fill="#BEA0F2"/>
|
||||||
|
<path d="M302.681 231.245C304.08 231.245 305.213 229.535 305.213 227.425C305.213 225.315 304.08 223.604 302.681 223.604C301.283 223.604 300.15 225.315 300.15 227.425C300.15 229.535 301.283 231.245 302.681 231.245Z" fill="#271B3D"/>
|
||||||
|
<path d="M128 227.739H225.415" stroke="#271B3D" stroke-miterlimit="10"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
|
@ -9,8 +9,13 @@ import { recordEvent } from '@woocommerce/tracks';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
ColorPaletteResponse,
|
||||||
designWithAiStateMachineContext,
|
designWithAiStateMachineContext,
|
||||||
designWithAiStateMachineEvents,
|
designWithAiStateMachineEvents,
|
||||||
|
FontPairing,
|
||||||
|
LookAndToneCompletionResponse,
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { aiWizardClosedBeforeCompletionEvent } from './events';
|
import { aiWizardClosedBeforeCompletionEvent } from './events';
|
||||||
import {
|
import {
|
||||||
|
@ -18,7 +23,6 @@ import {
|
||||||
lookAndFeelCompleteEvent,
|
lookAndFeelCompleteEvent,
|
||||||
toneOfVoiceCompleteEvent,
|
toneOfVoiceCompleteEvent,
|
||||||
} from './pages';
|
} from './pages';
|
||||||
import { LookAndToneCompletionResponse } from './services';
|
|
||||||
|
|
||||||
const assignBusinessInfoDescription = assign<
|
const assignBusinessInfoDescription = assign<
|
||||||
designWithAiStateMachineContext,
|
designWithAiStateMachineContext,
|
||||||
|
@ -72,14 +76,82 @@ const assignLookAndTone = assign<
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
const assignDefaultColorPalette = assign<
|
||||||
|
designWithAiStateMachineContext,
|
||||||
|
designWithAiStateMachineEvents
|
||||||
|
>( {
|
||||||
|
aiSuggestions: ( context, event: unknown ) => {
|
||||||
|
return {
|
||||||
|
...context.aiSuggestions,
|
||||||
|
defaultColorPalette: (
|
||||||
|
event as {
|
||||||
|
data: {
|
||||||
|
response: ColorPaletteResponse;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data.response,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const assignFontPairing = assign<
|
||||||
|
designWithAiStateMachineContext,
|
||||||
|
designWithAiStateMachineEvents
|
||||||
|
>( {
|
||||||
|
aiSuggestions: ( context, event: unknown ) => {
|
||||||
|
return {
|
||||||
|
...context.aiSuggestions,
|
||||||
|
fontPairing: (
|
||||||
|
event as {
|
||||||
|
data: {
|
||||||
|
response: FontPairing;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data.response.pair_name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const assignHeader = assign<
|
||||||
|
designWithAiStateMachineContext,
|
||||||
|
designWithAiStateMachineEvents
|
||||||
|
>( {
|
||||||
|
aiSuggestions: ( context, event: unknown ) => {
|
||||||
|
return {
|
||||||
|
...context.aiSuggestions,
|
||||||
|
header: (
|
||||||
|
event as {
|
||||||
|
data: {
|
||||||
|
response: Header;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data.response.slug,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const assignFooter = assign<
|
||||||
|
designWithAiStateMachineContext,
|
||||||
|
designWithAiStateMachineEvents
|
||||||
|
>( {
|
||||||
|
aiSuggestions: ( context, event: unknown ) => {
|
||||||
|
return {
|
||||||
|
...context.aiSuggestions,
|
||||||
|
footer: (
|
||||||
|
event as {
|
||||||
|
data: {
|
||||||
|
response: Footer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data.response.slug,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
const logAIAPIRequestError = () => {
|
const logAIAPIRequestError = () => {
|
||||||
// log AI API request error
|
// log AI API request error
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log( 'API Request error' );
|
console.log( 'API Request error' );
|
||||||
recordEvent(
|
|
||||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
|
||||||
{ error_type: 'http_network_error' }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateQueryStep = (
|
const updateQueryStep = (
|
||||||
|
@ -142,6 +214,10 @@ export const actions = {
|
||||||
assignLookAndFeel,
|
assignLookAndFeel,
|
||||||
assignToneOfVoice,
|
assignToneOfVoice,
|
||||||
assignLookAndTone,
|
assignLookAndTone,
|
||||||
|
assignDefaultColorPalette,
|
||||||
|
assignFontPairing,
|
||||||
|
assignHeader,
|
||||||
|
assignFooter,
|
||||||
logAIAPIRequestError,
|
logAIAPIRequestError,
|
||||||
updateQueryStep,
|
updateQueryStep,
|
||||||
recordTracksStepViewed,
|
recordTracksStepViewed,
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 20px; /* 125% */
|
line-height: 20px; /* 125% */
|
||||||
letter-spacing: -0.24px;
|
letter-spacing: -0.24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
|
@ -19,6 +19,8 @@ import {
|
||||||
} from './pages';
|
} from './pages';
|
||||||
import { customizeStoreStateMachineEvents } from '..';
|
import { customizeStoreStateMachineEvents } from '..';
|
||||||
|
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
export type events = { type: 'THEME_SUGGESTED' };
|
export type events = { type: 'THEME_SUGGESTED' };
|
||||||
export type DesignWithAiComponent =
|
export type DesignWithAiComponent =
|
||||||
| typeof BusinessInfoDescription
|
| typeof BusinessInfoDescription
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const LookAndFeel = ( {
|
||||||
title: __( 'Contemporary', 'woocommerce' ),
|
title: __( 'Contemporary', 'woocommerce' ),
|
||||||
key: 'Contemporary' as const,
|
key: 'Contemporary' as const,
|
||||||
subtitle: __(
|
subtitle: __(
|
||||||
'Clean lines, neutral colors, sleek and modern look',
|
'Clean lines, neutral colors, sleek and modern look.',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
@ -77,7 +77,10 @@ export const ToneOfVoice = ( {
|
||||||
<div className="woocommerce-cys-design-with-ai-tone-of-voice woocommerce-cys-layout">
|
<div className="woocommerce-cys-design-with-ai-tone-of-voice woocommerce-cys-layout">
|
||||||
<div className="woocommerce-cys-page">
|
<div className="woocommerce-cys-page">
|
||||||
<h1>
|
<h1>
|
||||||
{ __( 'How would you like to sound?', 'woocommerce' ) }
|
{ __(
|
||||||
|
'Which writing style do you prefer?',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
</h1>
|
</h1>
|
||||||
<div className="choices">
|
<div className="choices">
|
||||||
{ choices.map( ( { title, subtitle, key } ) => {
|
{ choices.map( ( { title, subtitle, key } ) => {
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
/**
|
||||||
|
* 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})$/;
|
||||||
|
const colorPaletteNameValidator = z
|
||||||
|
.string()
|
||||||
|
.refine( ( name ) => allowedNames.includes( name ), {
|
||||||
|
message: 'Color palette not part of allowed list',
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const colorPaletteValidator = z.object( {
|
||||||
|
name: colorPaletteNameValidator,
|
||||||
|
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 colorPaletteResponseValidator = z.object( {
|
||||||
|
default: colorPaletteNameValidator,
|
||||||
|
bestColors: z.array( colorPaletteNameValidator ).length( 8 ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
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, along with 8 best alternatives.
|
||||||
|
Respond in the form: "{ default: "palette name", bestColors: [ "palette name 1", "palette name 2", "palette name 3", "palette name 4", "palette name 5", "palette name 6", "palette name 7", "palette name 8" ] }"
|
||||||
|
|
||||||
|
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||||
|
Business description: ${ businessDescription }
|
||||||
|
|
||||||
|
Colors to choose from:
|
||||||
|
${ JSON.stringify( colorChoices ) }
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
responseValidation: colorPaletteResponseValidator.parse,
|
||||||
|
};
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
/** This block below was generated by ChatGPT using GPT-4 on 2023-09-18 */
|
||||||
|
/** Original source: plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts */
|
||||||
|
const fontChoices = [
|
||||||
|
{
|
||||||
|
pair_name: 'Bodoni Moda + Overpass',
|
||||||
|
fonts: {
|
||||||
|
'Bodoni Moda':
|
||||||
|
'A modern serif font with high contrast between thick and thin lines, commonly used for headings.',
|
||||||
|
Overpass:
|
||||||
|
'A clean, modern sans-serif, originally inspired by Highway Gothic. Good for text and UI elements.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Overpass is used for buttons and general typography, while Bodoni Moda is specified for headings and some core blocks like site title and post navigation link.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Commissioner + Crimson Pro',
|
||||||
|
fonts: {
|
||||||
|
Commissioner:
|
||||||
|
'A low-contrast, geometric sans-serif, designed for legibility and readability in long texts.',
|
||||||
|
'Crimson Pro':
|
||||||
|
'A serif typeface designed for readability and long-form text.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Commissioner dominates elements like buttons, headings, and core blocks, while Crimson Pro is set for general typography.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Libre Baskerville + DM Sans',
|
||||||
|
fonts: {
|
||||||
|
'Libre Baskerville':
|
||||||
|
'A serif typeface with a classic feel, good for long reading and often used for body text in books.',
|
||||||
|
'DM Sans':
|
||||||
|
'A clean, geometric sans-serif, often used for UI and short text.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Libre Baskerville is used for headings and core blocks, whereas DM Sans is used for buttons and general typography.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Libre Franklin + EB Garamond',
|
||||||
|
fonts: {
|
||||||
|
'Libre Franklin':
|
||||||
|
'A sans-serif that offers readability, suitable for both text and display.',
|
||||||
|
'EB Garamond':
|
||||||
|
"A revival of the classical 'Garamond' typefaces, suitable for long-form text.",
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Libre Franklin is predominantly used for elements like buttons, headings, and core blocks. EB Garamond is set for general typography.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Montserrat + Arvo',
|
||||||
|
fonts: {
|
||||||
|
Montserrat:
|
||||||
|
'A geometric sans-serif, popular for its modern clean lines.',
|
||||||
|
Arvo: 'A slab-serif font with a more traditional feel, suitable for print and screen.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Montserrat is used for buttons, headings, and core blocks. Arvo is used for general typography.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Playfair Display + Fira Sans',
|
||||||
|
fonts: {
|
||||||
|
'Playfair Display':
|
||||||
|
'A high-contrast serif designed for headings and offers a modern take on older serif fonts.',
|
||||||
|
'Fira Sans':
|
||||||
|
'A sans-serif designed for readability at small sizes, making it suitable for both UI and text.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Playfair Display is used in italics for headings and core blocks, while Fira Sans is used for buttons and general typography.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Rubik + Inter',
|
||||||
|
fonts: {
|
||||||
|
Rubik: 'A sans-serif with slightly rounded corners, designed for a softer, more modern look.',
|
||||||
|
Inter: 'A highly legible sans-serif, optimized for UI design.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Rubik is applied for headings and core blocks. Inter is used for buttons and general typography.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pair_name: 'Space Mono + Roboto',
|
||||||
|
fonts: {
|
||||||
|
'Space Mono': 'A monospace typeface with a futuristic vibe.',
|
||||||
|
Roboto: 'A neo-grotesque sans-serif, known for its flexibility and modern design.',
|
||||||
|
},
|
||||||
|
settings:
|
||||||
|
'Space Mono is used for headings, while Roboto takes care of buttons and general typography.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedFontChoices = fontChoices.map( ( config ) => config.pair_name );
|
||||||
|
|
||||||
|
export const fontChoiceValidator = z.object( {
|
||||||
|
pair_name: z
|
||||||
|
.string()
|
||||||
|
.refine( ( name ) => allowedFontChoices.includes( name ), {
|
||||||
|
message: 'Font choice not part of allowed list',
|
||||||
|
} ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const fontPairings = {
|
||||||
|
queryId: 'font_pairings',
|
||||||
|
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 font pairing.
|
||||||
|
Respond only with one font pairing and and in the format: '{"pair_name":"font 1 + font 2"}'.
|
||||||
|
|
||||||
|
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||||
|
Business description: ${ businessDescription }
|
||||||
|
|
||||||
|
Font pairings to choose from:
|
||||||
|
${ JSON.stringify( fontChoices ) }
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
responseValidation: fontChoiceValidator.parse,
|
||||||
|
};
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const footerChoices = [
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/footer-simple-menu-and-cart',
|
||||||
|
label: 'Footer with Simple Menu and Cart',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/footer-with-3-menus',
|
||||||
|
label: 'Footer with 3 Menus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/footer-large',
|
||||||
|
label: 'Large Footer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedFooter: string[] = footerChoices.map( ( footer ) => footer.slug );
|
||||||
|
|
||||||
|
export const footerValidator = z.object( {
|
||||||
|
slug: z.string().refine( ( slug ) => allowedFooter.includes( slug ), {
|
||||||
|
message: 'Footer not part of allowed list',
|
||||||
|
} ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const defaultFooter = {
|
||||||
|
queryId: 'default_footer',
|
||||||
|
|
||||||
|
// make sure version is updated every time the prompt is changed
|
||||||
|
version: '2023-09-19',
|
||||||
|
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 footer.
|
||||||
|
Respond only with one footer and only its JSON.
|
||||||
|
|
||||||
|
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||||
|
Business description: ${ businessDescription }
|
||||||
|
|
||||||
|
Footer to choose from:
|
||||||
|
${ JSON.stringify( footerChoices ) }
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
responseValidation: footerValidator.parse,
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const headerChoices = [
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-essential',
|
||||||
|
label: 'Essential Header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-centered-menu-with-search',
|
||||||
|
label: 'Centered Menu with search Header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-minimal',
|
||||||
|
label: 'Minimal Header',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'woocommerce-blocks/header-large',
|
||||||
|
label: 'Large Header',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedHeaders: string[] = headerChoices.map( ( header ) => header.slug );
|
||||||
|
|
||||||
|
export const headerValidator = z.object( {
|
||||||
|
slug: z.string().refine( ( slug ) => allowedHeaders.includes( slug ), {
|
||||||
|
message: 'Header not part of allowed list',
|
||||||
|
} ),
|
||||||
|
} );
|
||||||
|
|
||||||
|
export const defaultHeader = {
|
||||||
|
queryId: 'default_header',
|
||||||
|
|
||||||
|
// make sure version is updated every time the prompt is changed
|
||||||
|
version: '2023-09-19',
|
||||||
|
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 header.
|
||||||
|
Respond only with one header and only its JSON.
|
||||||
|
|
||||||
|
Chosen look and tone: ${ look } look, ${ tone } tone.
|
||||||
|
Business description: ${ businessDescription }
|
||||||
|
|
||||||
|
Headers to choose from:
|
||||||
|
${ JSON.stringify( headerChoices ) }
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
responseValidation: headerValidator.parse,
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './colorChoices';
|
||||||
|
export * from './lookAndTone';
|
||||||
|
export * from './fontPairings';
|
||||||
|
export * from './header';
|
||||||
|
export * from './footer';
|
|
@ -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'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,238 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { colorPaletteValidator, defaultColorPalette } from '..';
|
||||||
|
|
||||||
|
describe( 'colorPaletteValidator', () => {
|
||||||
|
it( 'should validate a correct color palette', () => {
|
||||||
|
const validPalette = {
|
||||||
|
name: 'Ancient Bronze',
|
||||||
|
primary: '#11163d',
|
||||||
|
secondary: '#8C8369',
|
||||||
|
foreground: '#11163d',
|
||||||
|
background: '#ffffff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedResult = colorPaletteValidator.parse( 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( () => colorPaletteValidator.parse( 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( () => colorPaletteValidator.parse( 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( () => colorPaletteValidator.parse( 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( () => colorPaletteValidator.parse( 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( () => colorPaletteValidator.parse( invalidPalette ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"validation\\": \\"regex\\",
|
||||||
|
\\"code\\": \\"invalid_string\\",
|
||||||
|
\\"message\\": \\"Invalid background color\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"background\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'colorPaletteResponseValidator', () => {
|
||||||
|
it( 'should validate a correct color palette response', () => {
|
||||||
|
const validPalette = {
|
||||||
|
default: 'Ancient Bronze',
|
||||||
|
bestColors: Array( 8 ).fill( 'Ancient Bronze' ),
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedResult =
|
||||||
|
defaultColorPalette.responseValidation( validPalette );
|
||||||
|
expect( parsedResult ).toEqual( validPalette );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should fail if array contains invalid color', () => {
|
||||||
|
const invalidPalette = {
|
||||||
|
default: 'Ancient Bronze',
|
||||||
|
bestColors: Array( 7 )
|
||||||
|
.fill( 'Ancient Bronze' )
|
||||||
|
.concat( [ 'Invalid Color' ] ),
|
||||||
|
};
|
||||||
|
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"custom\\",
|
||||||
|
\\"message\\": \\"Color palette not part of allowed list\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"bestColors\\",
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should fail if bestColors property is missing', () => {
|
||||||
|
const invalidPalette = {
|
||||||
|
default: 'Ancient Bronze',
|
||||||
|
};
|
||||||
|
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"array\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"bestColors\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
it( 'should fail if default property is missing', () => {
|
||||||
|
const invalidPalette = {
|
||||||
|
bestColors: Array( 8 ).fill( 'Ancient Bronze' ),
|
||||||
|
};
|
||||||
|
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"undefined\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"default\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Required\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should fail if bestColors array is not of length 8', () => {
|
||||||
|
const invalidPalette = {
|
||||||
|
default: 'Ancient Bronze',
|
||||||
|
bestColors: Array( 7 ).fill( 'Ancient Bronze' ),
|
||||||
|
};
|
||||||
|
expect( () => defaultColorPalette.responseValidation( invalidPalette ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"too_small\\",
|
||||||
|
\\"minimum\\": 8,
|
||||||
|
\\"type\\": \\"array\\",
|
||||||
|
\\"inclusive\\": true,
|
||||||
|
\\"exact\\": true,
|
||||||
|
\\"message\\": \\"Array must contain exactly 8 element(s)\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"bestColors\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { fontChoiceValidator } from '..';
|
||||||
|
|
||||||
|
describe( 'fontChoiceValidator', () => {
|
||||||
|
it( 'should validate when font choice is part of the allowed list', () => {
|
||||||
|
const validFontChoice = { pair_name: 'Montserrat + Arvo' };
|
||||||
|
expect( () =>
|
||||||
|
fontChoiceValidator.parse( validFontChoice )
|
||||||
|
).not.toThrow();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when font choice is not part of the allowed list', () => {
|
||||||
|
const invalidFontChoice = { pair_name: 'Comic Sans' };
|
||||||
|
expect( () => fontChoiceValidator.parse( invalidFontChoice ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"custom\\",
|
||||||
|
\\"message\\": \\"Font choice not part of allowed list\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"pair_name\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when pair_name is not a string', () => {
|
||||||
|
const invalidType = { pair_name: 123 };
|
||||||
|
expect( () => fontChoiceValidator.parse( invalidType ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"number\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"pair_name\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Expected string, received number\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { footerValidator } from '..';
|
||||||
|
|
||||||
|
describe( 'footerValidator', () => {
|
||||||
|
it( 'should validate when footer is part of the allowed list', () => {
|
||||||
|
const validFooter = { slug: 'woocommerce-blocks/footer-large' };
|
||||||
|
expect( () => footerValidator.parse( validFooter ) ).not.toThrow();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when footer is not part of the allowed list', () => {
|
||||||
|
const invalidFooter = {
|
||||||
|
slug: 'woocommerce-blocks/footer-large-invalid',
|
||||||
|
};
|
||||||
|
expect( () => footerValidator.parse( invalidFooter ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"custom\\",
|
||||||
|
\\"message\\": \\"Footer not part of allowed list\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when slug is not a string', () => {
|
||||||
|
const invalidType = { slug: 123 };
|
||||||
|
expect( () => footerValidator.parse( invalidType ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"number\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Expected string, received number\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { headerValidator } from '..';
|
||||||
|
|
||||||
|
describe( 'headerValidator', () => {
|
||||||
|
it( 'should validate when header is part of the allowed list', () => {
|
||||||
|
const validHeader = { slug: 'woocommerce-blocks/header-large' };
|
||||||
|
expect( () => headerValidator.parse( validHeader ) ).not.toThrow();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when header is not part of the allowed list', () => {
|
||||||
|
const invalidHeader = {
|
||||||
|
slug: 'woocommerce-blocks/header-large-invalid',
|
||||||
|
};
|
||||||
|
expect( () => headerValidator.parse( invalidHeader ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"custom\\",
|
||||||
|
\\"message\\": \\"Header not part of allowed list\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should not validate when slug is not a string', () => {
|
||||||
|
const invalidType = { slug: 123 };
|
||||||
|
expect( () => headerValidator.parse( invalidType ) )
|
||||||
|
.toThrowErrorMatchingInlineSnapshot( `
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"code\\": \\"invalid_type\\",
|
||||||
|
\\"expected\\": \\"string\\",
|
||||||
|
\\"received\\": \\"number\\",
|
||||||
|
\\"path\\": [
|
||||||
|
\\"slug\\"
|
||||||
|
],
|
||||||
|
\\"message\\": \\"Expected string, received number\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
` );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -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' );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -1,106 +1,32 @@
|
||||||
|
/* eslint-disable @woocommerce/dependency-group */
|
||||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
|
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
|
||||||
import apiFetch from '@wordpress/api-fetch';
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { Sender } from 'xstate';
|
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
import { Sender, assign, createMachine } from 'xstate';
|
||||||
|
import { dispatch, resolveSelect } from '@wordpress/data';
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
import { store as coreStore } from '@wordpress/core-data';
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
import { mergeBaseAndUserConfigs } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { designWithAiStateMachineContext } from './types';
|
||||||
|
import { lookAndTone } from './prompts';
|
||||||
|
import { FONT_PAIRINGS } from '../assembler-hub/sidebar/global-styles/font-pairing-variations/constants';
|
||||||
|
import { COLOR_PALETTES } from '../assembler-hub/sidebar/global-styles/color-palette-variations/constants';
|
||||||
import {
|
import {
|
||||||
Look,
|
patternsToNameMap,
|
||||||
Tone,
|
getTemplatePatterns,
|
||||||
VALID_LOOKS,
|
LARGE_BUSINESS_TEMPLATES,
|
||||||
VALID_TONES,
|
SMALL_MEDIUM_BUSINESS_TEMPLATES,
|
||||||
designWithAiStateMachineContext,
|
} from '../assembler-hub/hooks/use-home-templates';
|
||||||
} 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 );
|
|
||||||
};
|
|
||||||
|
|
||||||
const browserPopstateHandler =
|
const browserPopstateHandler =
|
||||||
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
|
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
|
||||||
|
@ -113,7 +39,354 @@ 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 updateStorePatterns = async (
|
||||||
|
context: designWithAiStateMachineContext
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// TODO: Probably move this to a more appropriate place with a check. We should set this when the user granted permissions during the onboarding phase.
|
||||||
|
await dispatch( OPTIONS_STORE_NAME ).updateOptions( {
|
||||||
|
woocommerce_blocks_allow_ai_connection: true,
|
||||||
|
} );
|
||||||
|
|
||||||
|
await apiFetch( {
|
||||||
|
path: '/wc/store/patterns',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
business_description:
|
||||||
|
context.businessInfoDescription.descriptionText,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} catch ( error ) {
|
||||||
|
recordEvent( 'customize_your_store_update_store_pattern_api_error', {
|
||||||
|
error: error instanceof Error ? error.message : 'unknown',
|
||||||
|
} );
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the current global styles of theme
|
||||||
|
const updateGlobalStyles = async ( {
|
||||||
|
colorPaletteName = COLOR_PALETTES[ 0 ].title,
|
||||||
|
fontPairingName = FONT_PAIRINGS[ 0 ].title,
|
||||||
|
}: {
|
||||||
|
colorPaletteName: string;
|
||||||
|
fontPairingName: string;
|
||||||
|
} ) => {
|
||||||
|
const colorPalette = COLOR_PALETTES.find(
|
||||||
|
( palette ) => palette.title === colorPaletteName
|
||||||
|
);
|
||||||
|
const fontPairing = FONT_PAIRINGS.find(
|
||||||
|
( pairing ) => pairing.title === fontPairingName
|
||||||
|
);
|
||||||
|
|
||||||
|
const globalStylesId = await resolveSelect(
|
||||||
|
coreStore
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
).__experimentalGetCurrentGlobalStylesId();
|
||||||
|
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
const { saveEntityRecord } = dispatch( coreStore );
|
||||||
|
|
||||||
|
await saveEntityRecord(
|
||||||
|
'root',
|
||||||
|
'globalStyles',
|
||||||
|
{
|
||||||
|
id: globalStylesId,
|
||||||
|
styles: mergeBaseAndUserConfigs(
|
||||||
|
colorPalette?.styles || {},
|
||||||
|
fontPairing?.styles || {}
|
||||||
|
),
|
||||||
|
settings: mergeBaseAndUserConfigs(
|
||||||
|
colorPalette?.settings || {},
|
||||||
|
fontPairing?.settings || {}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
throwOnError: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the current theme template
|
||||||
|
const updateTemplate = async ( {
|
||||||
|
headerSlug,
|
||||||
|
businessSize,
|
||||||
|
homepageTemplateId,
|
||||||
|
footerSlug,
|
||||||
|
}: {
|
||||||
|
headerSlug: string;
|
||||||
|
businessSize: 'SMB' | 'LB';
|
||||||
|
homepageTemplateId:
|
||||||
|
| keyof typeof SMALL_MEDIUM_BUSINESS_TEMPLATES
|
||||||
|
| keyof typeof LARGE_BUSINESS_TEMPLATES;
|
||||||
|
footerSlug: string;
|
||||||
|
} ) => {
|
||||||
|
const patterns = ( await resolveSelect(
|
||||||
|
coreStore
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
).getBlockPatterns() ) as Pattern[];
|
||||||
|
|
||||||
|
const patternsByName = patternsToNameMap( patterns );
|
||||||
|
|
||||||
|
const headerPattern = patternsByName[ headerSlug ];
|
||||||
|
const footerPattern = patternsByName[ footerSlug ];
|
||||||
|
|
||||||
|
const homepageTemplate = getTemplatePatterns(
|
||||||
|
businessSize === 'SMB'
|
||||||
|
? SMALL_MEDIUM_BUSINESS_TEMPLATES[ homepageTemplateId ]
|
||||||
|
: LARGE_BUSINESS_TEMPLATES[ homepageTemplateId ],
|
||||||
|
patternsByName
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = [ headerPattern, ...homepageTemplate, footerPattern ]
|
||||||
|
.filter( Boolean )
|
||||||
|
.map( ( pattern ) => pattern.content )
|
||||||
|
.join( '\n\n' );
|
||||||
|
|
||||||
|
const currentTemplate = await resolveSelect(
|
||||||
|
coreStore
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
).__experimentalGetTemplateForLink( '/' );
|
||||||
|
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
const { saveEntityRecord } = dispatch( coreStore );
|
||||||
|
|
||||||
|
await saveEntityRecord(
|
||||||
|
'postType',
|
||||||
|
currentTemplate.type,
|
||||||
|
{
|
||||||
|
id: currentTemplate.id,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
throwOnError: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assembleSite = async (
|
||||||
|
context: designWithAiStateMachineContext
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await updateGlobalStyles( {
|
||||||
|
colorPaletteName: context.aiSuggestions.defaultColorPalette.default,
|
||||||
|
fontPairingName: context.aiSuggestions.fontPairing,
|
||||||
|
} );
|
||||||
|
recordEvent( 'customize_your_store_ai_update_global_styles_success' );
|
||||||
|
} catch ( error ) {
|
||||||
|
// TODO handle error
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error( error );
|
||||||
|
recordEvent(
|
||||||
|
'customize_your_store_ai_update_global_styles_response_error',
|
||||||
|
{
|
||||||
|
error: error instanceof Error ? error.message : 'unknown',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateTemplate( {
|
||||||
|
headerSlug: context.aiSuggestions.header,
|
||||||
|
// TODO: Get from context
|
||||||
|
businessSize: 'SMB',
|
||||||
|
homepageTemplateId: 'template1',
|
||||||
|
footerSlug: context.aiSuggestions.footer,
|
||||||
|
} );
|
||||||
|
recordEvent( 'customize_your_store_ai_update_template_success' );
|
||||||
|
} catch ( error ) {
|
||||||
|
// TODO handle error
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error( error );
|
||||||
|
recordEvent( 'customize_your_store_ai_update_template_response_error', {
|
||||||
|
error: error instanceof Error ? error.message : 'unknown',
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore No types for this exist yet.
|
||||||
|
const { invalidateResolutionForStoreSelector } = dispatch( coreStore );
|
||||||
|
|
||||||
|
// Invalid the selectors so that the new template/style are used in assembler hub.
|
||||||
|
invalidateResolutionForStoreSelector( 'getEntityRecord' );
|
||||||
|
invalidateResolutionForStoreSelector(
|
||||||
|
'__experimentalGetCurrentGlobalStylesId'
|
||||||
|
);
|
||||||
|
invalidateResolutionForStoreSelector( '__experimentalGetTemplateForLink' );
|
||||||
|
};
|
||||||
|
|
||||||
export const services = {
|
export const services = {
|
||||||
getLookAndTone,
|
getLookAndTone,
|
||||||
browserPopstateHandler,
|
browserPopstateHandler,
|
||||||
|
queryAiEndpoint,
|
||||||
|
assembleSite,
|
||||||
|
updateStorePatterns,
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,10 @@ import { getQuery } from '@woocommerce/navigation';
|
||||||
import {
|
import {
|
||||||
designWithAiStateMachineContext,
|
designWithAiStateMachineContext,
|
||||||
designWithAiStateMachineEvents,
|
designWithAiStateMachineEvents,
|
||||||
|
FontPairing,
|
||||||
|
Header,
|
||||||
|
Footer,
|
||||||
|
ColorPaletteResponse,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
BusinessInfoDescription,
|
BusinessInfoDescription,
|
||||||
|
@ -19,6 +23,12 @@ import {
|
||||||
} from './pages';
|
} from './pages';
|
||||||
import { actions } from './actions';
|
import { actions } from './actions';
|
||||||
import { services } from './services';
|
import { services } from './services';
|
||||||
|
import {
|
||||||
|
defaultColorPalette,
|
||||||
|
fontPairings,
|
||||||
|
defaultHeader,
|
||||||
|
defaultFooter,
|
||||||
|
} from './prompts';
|
||||||
|
|
||||||
export const hasStepInUrl = (
|
export const hasStepInUrl = (
|
||||||
_ctx: unknown,
|
_ctx: unknown,
|
||||||
|
@ -60,13 +70,18 @@ export const designWithAiStateMachineDefinition = createMachine(
|
||||||
businessInfoDescription: {
|
businessInfoDescription: {
|
||||||
descriptionText: '',
|
descriptionText: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
lookAndFeel: {
|
lookAndFeel: {
|
||||||
choice: '',
|
choice: '',
|
||||||
},
|
},
|
||||||
toneOfVoice: {
|
toneOfVoice: {
|
||||||
choice: '',
|
choice: '',
|
||||||
},
|
},
|
||||||
|
aiSuggestions: {
|
||||||
|
defaultColorPalette: {} as ColorPaletteResponse,
|
||||||
|
fontPairing: '' as FontPairing[ 'pair_name' ],
|
||||||
|
header: '' as Header[ 'slug' ],
|
||||||
|
footer: '' as Footer[ 'slug' ],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
initial: 'navigate',
|
initial: 'navigate',
|
||||||
states: {
|
states: {
|
||||||
|
@ -264,8 +279,161 @@ export const designWithAiStateMachineDefinition = createMachine(
|
||||||
step: 'api-call-loader',
|
step: 'api-call-loader',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
type: 'parallel',
|
||||||
|
states: {
|
||||||
|
chooseColorPairing: {
|
||||||
|
initial: 'pending',
|
||||||
|
states: {
|
||||||
|
pending: {
|
||||||
|
invoke: {
|
||||||
|
src: 'queryAiEndpoint',
|
||||||
|
data: ( context ) => {
|
||||||
|
return {
|
||||||
|
...defaultColorPalette,
|
||||||
|
prompt: defaultColorPalette.prompt(
|
||||||
|
context
|
||||||
|
.businessInfoDescription
|
||||||
|
.descriptionText,
|
||||||
|
context.lookAndFeel
|
||||||
|
.choice,
|
||||||
|
context.toneOfVoice
|
||||||
|
.choice
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
actions: [
|
||||||
|
'assignDefaultColorPalette',
|
||||||
|
],
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chooseFontPairing: {
|
||||||
|
initial: 'pending',
|
||||||
|
states: {
|
||||||
|
pending: {
|
||||||
|
invoke: {
|
||||||
|
src: 'queryAiEndpoint',
|
||||||
|
data: ( context ) => {
|
||||||
|
return {
|
||||||
|
...fontPairings,
|
||||||
|
prompt: fontPairings.prompt(
|
||||||
|
context
|
||||||
|
.businessInfoDescription
|
||||||
|
.descriptionText,
|
||||||
|
context.lookAndFeel
|
||||||
|
.choice,
|
||||||
|
context.toneOfVoice
|
||||||
|
.choice
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
actions: [
|
||||||
|
'assignFontPairing',
|
||||||
|
],
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chooseHeader: {
|
||||||
|
initial: 'pending',
|
||||||
|
states: {
|
||||||
|
pending: {
|
||||||
|
invoke: {
|
||||||
|
src: 'queryAiEndpoint',
|
||||||
|
data: ( context ) => {
|
||||||
|
return {
|
||||||
|
...defaultHeader,
|
||||||
|
prompt: defaultHeader.prompt(
|
||||||
|
context
|
||||||
|
.businessInfoDescription
|
||||||
|
.descriptionText,
|
||||||
|
context.lookAndFeel
|
||||||
|
.choice,
|
||||||
|
context.toneOfVoice
|
||||||
|
.choice
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
actions: [ 'assignHeader' ],
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chooseFooter: {
|
||||||
|
initial: 'pending',
|
||||||
|
states: {
|
||||||
|
pending: {
|
||||||
|
invoke: {
|
||||||
|
src: 'queryAiEndpoint',
|
||||||
|
data: ( context ) => {
|
||||||
|
return {
|
||||||
|
...defaultFooter,
|
||||||
|
prompt: defaultFooter.prompt(
|
||||||
|
context
|
||||||
|
.businessInfoDescription
|
||||||
|
.descriptionText,
|
||||||
|
context.lookAndFeel
|
||||||
|
.choice,
|
||||||
|
context.toneOfVoice
|
||||||
|
.choice
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
actions: [ 'assignFooter' ],
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateStorePatterns: {
|
||||||
|
initial: 'pending',
|
||||||
|
states: {
|
||||||
|
pending: {
|
||||||
|
invoke: {
|
||||||
|
src: 'updateStorePatterns',
|
||||||
|
onDone: {
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
// TODO: handle error
|
||||||
|
target: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success: { type: 'final' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onDone: 'postApiCallLoader',
|
||||||
|
},
|
||||||
|
postApiCallLoader: {
|
||||||
|
invoke: {
|
||||||
|
src: 'assembleSite',
|
||||||
|
onDone: {
|
||||||
|
actions: [
|
||||||
|
sendParent( () => ( {
|
||||||
|
type: 'THEME_SUGGESTED',
|
||||||
|
} ) ),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
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,154 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
|
||||||
|
import apiFetch from '@wordpress/api-fetch';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import { getCompletion } from '../services';
|
||||||
parseLookAndToneCompletionResponse,
|
|
||||||
LookAndToneCompletionResponse,
|
|
||||||
} from '../services';
|
|
||||||
|
|
||||||
jest.mock( '@woocommerce/tracks', () => ( {
|
jest.mock( '@woocommerce/tracks', () => ( {
|
||||||
recordEvent: jest.fn(),
|
recordEvent: jest.fn(),
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
describe( 'parseLookAndToneCompletionResponse', () => {
|
jest.mock( '@woocommerce/ai', () => ( {
|
||||||
|
__experimentalRequestJetpackToken: jest.fn(),
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
jest.mock( '@wordpress/api-fetch', () => jest.fn() );
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'@wordpress/edit-site/build-module/components/global-styles/global-styles-provider',
|
||||||
|
() => ( {
|
||||||
|
mergeBaseAndUserConfigs: jest.fn(),
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
|
||||||
|
describe( 'getCompletion', () => {
|
||||||
beforeEach( () => {
|
beforeEach( () => {
|
||||||
( recordEvent as jest.Mock ).mockClear();
|
jest.clearAllMocks();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should return a valid object when given valid JSON', () => {
|
it( 'should successfully get completion', async () => {
|
||||||
const validObj = {
|
( requestJetpackToken as jest.Mock ).mockResolvedValue( {
|
||||||
completion: '{"look": "Contemporary", "tone": "Neutral"}',
|
token: 'fake_token',
|
||||||
};
|
} );
|
||||||
const result = parseLookAndToneCompletionResponse( validObj );
|
( apiFetch as unknown as jest.Mock ).mockResolvedValue( {
|
||||||
const expected: LookAndToneCompletionResponse = {
|
completion: JSON.stringify( { key: 'value' } ),
|
||||||
look: 'Contemporary',
|
} );
|
||||||
tone: 'Neutral',
|
const responseValidation = jest.fn( ( json ) => json );
|
||||||
};
|
|
||||||
expect( result ).toEqual( expected );
|
const result = await getCompletion( {
|
||||||
expect( recordEvent ).not.toHaveBeenCalled();
|
queryId: 'query1',
|
||||||
|
prompt: 'test prompt',
|
||||||
|
responseValidation,
|
||||||
|
retryCount: 0,
|
||||||
|
version: '1',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should throw an error and record an event for JSON parse error', () => {
|
expect( result ).toEqual( { key: 'value' } );
|
||||||
const invalidObj = { completion: 'invalid JSON' };
|
expect( responseValidation ).toBeCalledWith( { key: 'value' } );
|
||||||
expect( () =>
|
expect( recordEvent ).toBeCalledWith(
|
||||||
parseLookAndToneCompletionResponse( invalidObj )
|
'customize_your_store_ai_completion_success',
|
||||||
).toThrow( 'Could not parse Look and Tone completion response.' );
|
|
||||||
expect( recordEvent ).toHaveBeenCalledWith(
|
|
||||||
'customize_your_store_look_and_tone_ai_completion_response_error',
|
|
||||||
{
|
{
|
||||||
error_type: 'json_parse_error',
|
query_id: 'query1',
|
||||||
response: JSON.stringify( invalidObj ),
|
retry_count: 0,
|
||||||
|
version: '1',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should throw an error and record an event for valid JSON but invalid values', () => {
|
it( 'should handle API fetch error', async () => {
|
||||||
const invalidValuesObj = {
|
( requestJetpackToken as jest.Mock ).mockResolvedValue( {
|
||||||
completion: '{"look": "Invalid", "tone": "Invalid"}',
|
token: 'fake_token',
|
||||||
};
|
} );
|
||||||
expect( () =>
|
( apiFetch as unknown as jest.Mock ).mockRejectedValue(
|
||||||
parseLookAndToneCompletionResponse( invalidValuesObj )
|
new Error( 'API error' )
|
||||||
).toThrow( 'Could not parse Look and Tone completion response.' );
|
);
|
||||||
expect( recordEvent ).toHaveBeenCalledWith(
|
|
||||||
'customize_your_store_look_and_tone_ai_completion_response_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',
|
error_type: 'valid_json_invalid_values',
|
||||||
response: JSON.stringify( invalidValuesObj ),
|
response: JSON.stringify( { key: 'invalid value' } ),
|
||||||
|
version: '1',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { z } from 'zod';
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
colorPaletteValidator,
|
||||||
|
fontChoiceValidator,
|
||||||
|
headerValidator,
|
||||||
|
footerValidator,
|
||||||
|
colorPaletteResponseValidator,
|
||||||
|
} from './prompts';
|
||||||
|
|
||||||
export type designWithAiStateMachineContext = {
|
export type designWithAiStateMachineContext = {
|
||||||
businessInfoDescription: {
|
businessInfoDescription: {
|
||||||
descriptionText: string;
|
descriptionText: string;
|
||||||
|
@ -8,6 +23,12 @@ export type designWithAiStateMachineContext = {
|
||||||
toneOfVoice: {
|
toneOfVoice: {
|
||||||
choice: Tone | '';
|
choice: Tone | '';
|
||||||
};
|
};
|
||||||
|
aiSuggestions: {
|
||||||
|
defaultColorPalette: ColorPaletteResponse;
|
||||||
|
fontPairing: FontPairing[ 'pair_name' ];
|
||||||
|
header: Header[ 'slug' ];
|
||||||
|
footer: Footer[ 'slug' ];
|
||||||
|
};
|
||||||
// If we require more data from options, previously provided core profiler details,
|
// If we require more data from options, previously provided core profiler details,
|
||||||
// we can retrieve them in preBusinessInfoDescription and then assign them here
|
// we can retrieve them in preBusinessInfoDescription and then assign them here
|
||||||
};
|
};
|
||||||
|
@ -31,3 +52,19 @@ export const VALID_LOOKS = [ 'Contemporary', 'Classic', 'Bold' ] as const;
|
||||||
export const VALID_TONES = [ 'Informal', 'Neutral', 'Formal' ] as const;
|
export const VALID_TONES = [ 'Informal', 'Neutral', 'Formal' ] as const;
|
||||||
export type Look = ( typeof VALID_LOOKS )[ number ];
|
export type Look = ( typeof VALID_LOOKS )[ number ];
|
||||||
export type Tone = ( typeof VALID_TONES )[ number ];
|
export type Tone = ( typeof VALID_TONES )[ number ];
|
||||||
|
|
||||||
|
export interface LookAndToneCompletionResponse {
|
||||||
|
look: Look;
|
||||||
|
tone: Tone;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ColorPalette = z.infer< typeof colorPaletteValidator >;
|
||||||
|
export type ColorPaletteResponse = z.infer<
|
||||||
|
typeof colorPaletteResponseValidator
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type FontPairing = z.infer< typeof fontChoiceValidator >;
|
||||||
|
|
||||||
|
export type Header = z.infer< typeof headerValidator >;
|
||||||
|
|
||||||
|
export type Footer = z.infer< typeof footerValidator >;
|
||||||
|
|
|
@ -4,7 +4,13 @@
|
||||||
import { Sender, createMachine } from 'xstate';
|
import { Sender, createMachine } from 'xstate';
|
||||||
import { useEffect, useMemo, useState } from '@wordpress/element';
|
import { useEffect, useMemo, useState } from '@wordpress/element';
|
||||||
import { useMachine, useSelector } from '@xstate/react';
|
import { useMachine, useSelector } from '@xstate/react';
|
||||||
import { getQuery, updateQueryString } from '@woocommerce/navigation';
|
import {
|
||||||
|
getNewPath,
|
||||||
|
getQuery,
|
||||||
|
updateQueryString,
|
||||||
|
} from '@woocommerce/navigation';
|
||||||
|
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
import { dispatch } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -57,6 +63,16 @@ const updateQueryStep = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const redirectToWooHome = () => {
|
||||||
|
window.location.href = getNewPath( {}, '/', {} );
|
||||||
|
};
|
||||||
|
|
||||||
|
const markTaskComplete = async () => {
|
||||||
|
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
|
||||||
|
woocommerce_admin_customize_store_completed: 'yes',
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
const browserPopstateHandler =
|
const browserPopstateHandler =
|
||||||
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
|
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
|
||||||
const popstateHandler = () => {
|
const popstateHandler = () => {
|
||||||
|
@ -70,6 +86,7 @@ const browserPopstateHandler =
|
||||||
|
|
||||||
export const machineActions = {
|
export const machineActions = {
|
||||||
updateQueryStep,
|
updateQueryStep,
|
||||||
|
redirectToWooHome,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const customizeStoreStateMachineActions = {
|
export const customizeStoreStateMachineActions = {
|
||||||
|
@ -81,6 +98,7 @@ export const customizeStoreStateMachineServices = {
|
||||||
...introServices,
|
...introServices,
|
||||||
...transitionalServices,
|
...transitionalServices,
|
||||||
browserPopstateHandler,
|
browserPopstateHandler,
|
||||||
|
markTaskComplete,
|
||||||
};
|
};
|
||||||
export const customizeStoreStateMachineDefinition = createMachine( {
|
export const customizeStoreStateMachineDefinition = createMachine( {
|
||||||
id: 'customizeStore',
|
id: 'customizeStore',
|
||||||
|
@ -175,7 +193,7 @@ export const customizeStoreStateMachineDefinition = createMachine( {
|
||||||
target: 'assemblerHub',
|
target: 'assemblerHub',
|
||||||
},
|
},
|
||||||
CLICKED_ON_BREADCRUMB: {
|
CLICKED_ON_BREADCRUMB: {
|
||||||
target: 'backToHomescreen',
|
actions: 'redirectToWooHome',
|
||||||
},
|
},
|
||||||
SELECTED_NEW_THEME: {
|
SELECTED_NEW_THEME: {
|
||||||
target: 'appearanceTask',
|
target: 'appearanceTask',
|
||||||
|
@ -220,6 +238,14 @@ export const customizeStoreStateMachineDefinition = createMachine( {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
postAssemblerHub: {
|
postAssemblerHub: {
|
||||||
|
invoke: {
|
||||||
|
src: 'markTaskComplete',
|
||||||
|
onDone: {
|
||||||
|
target: 'waitForSitePreview',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
waitForSitePreview: {
|
||||||
after: {
|
after: {
|
||||||
// Wait for 5 seconds before redirecting to the transitional page. This is to ensure that the site preview image is refreshed.
|
// Wait for 5 seconds before redirecting to the transitional page. This is to ensure that the site preview image is refreshed.
|
||||||
5000: {
|
5000: {
|
||||||
|
@ -246,11 +272,10 @@ export const customizeStoreStateMachineDefinition = createMachine( {
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
GO_BACK_TO_HOME: {
|
GO_BACK_TO_HOME: {
|
||||||
target: 'backToHomescreen',
|
actions: 'redirectToWooHome',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
backToHomescreen: {},
|
|
||||||
appearanceTask: {},
|
appearanceTask: {},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { chevronLeft } from '@wordpress/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { CustomizeStoreComponent } from '../types';
|
import { CustomizeStoreComponent } from '../types';
|
||||||
|
|
||||||
|
import './intro.scss';
|
||||||
|
|
||||||
export type events =
|
export type events =
|
||||||
| { type: 'DESIGN_WITH_AI' }
|
| { type: 'DESIGN_WITH_AI' }
|
||||||
| { type: 'CLICKED_ON_BREADCRUMB' }
|
| { type: 'CLICKED_ON_BREADCRUMB' }
|
||||||
|
@ -15,33 +24,106 @@ export * as services from './services';
|
||||||
|
|
||||||
export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
|
export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
|
||||||
const {
|
const {
|
||||||
intro: { themeCards, activeTheme },
|
intro: { themeCards },
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Intro</h1>
|
<div className="woocommerce-customize-store-header">
|
||||||
<div>Active theme: { activeTheme }</div>
|
<h1>{ 'Site title' }</h1>
|
||||||
{ themeCards?.map( ( themeCard ) => (
|
</div>
|
||||||
|
|
||||||
|
<div className="woocommerce-customize-store-container">
|
||||||
|
<div className="woocommerce-customize-store-sidebar">
|
||||||
|
<div className="woocommerce-customize-store-sidebar__title">
|
||||||
|
<button
|
||||||
|
onClick={ () => {
|
||||||
|
sendEvent( 'CLICKED_ON_BREADCRUMB' );
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ chevronLeft }
|
||||||
|
</button>
|
||||||
|
{ __( 'Customize your store', 'woocommerce' ) }
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{ __(
|
||||||
|
'Create a store that reflects your brand and business. Select one of our professionally designed themes to customize, or create your own using AI.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="woocommerce-customize-store-main">
|
||||||
|
<div className="woocommerce-customize-store-banner">
|
||||||
|
<div
|
||||||
|
className={ `woocommerce-customize-store-banner-content` }
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
{ __(
|
||||||
|
'Use the power of AI to design your store',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
{ __(
|
||||||
|
'Design the look of your store, create pages, and generate copy using our built-in AI tools.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={ () =>
|
||||||
|
sendEvent( { type: 'DESIGN_WITH_AI' } )
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ __( 'Design with AI', 'woocommerce' ) }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{ __(
|
||||||
|
'Or select a professionally designed theme to customize and make your own.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="woocommerce-customize-store-theme-cards">
|
||||||
|
{ themeCards?.map( ( themeCard ) => (
|
||||||
|
<div className="theme-card" key={ themeCard.slug }>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={ themeCard.image }
|
||||||
|
alt={ themeCard.description }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2 className="theme-card__title">
|
||||||
|
{ themeCard.name }
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="woocommerce-customize-store-browse-themes">
|
||||||
<button
|
<button
|
||||||
key={ themeCard.name }
|
|
||||||
onClick={ () =>
|
onClick={ () =>
|
||||||
sendEvent( {
|
sendEvent( {
|
||||||
type: 'SELECTED_NEW_THEME',
|
type: 'SELECTED_BROWSE_ALL_THEMES',
|
||||||
payload: { theme: themeCard.name },
|
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{ themeCard.name }
|
{ __( 'Browse all themes', 'woocommerce' ) }
|
||||||
</button>
|
|
||||||
) ) }
|
|
||||||
<button onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }>
|
|
||||||
Design with AI
|
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={ () => sendEvent( { type: 'SELECTED_ACTIVE_THEME' } ) }
|
onClick={ () =>
|
||||||
|
sendEvent( { type: 'SELECTED_ACTIVE_THEME' } )
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Assembler Hub
|
Assembler Hub
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
.woocommerce-profile-wizard__container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-header {
|
||||||
|
min-height: 64px;
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-sidebar {
|
||||||
|
flex: 0 0 380px;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.woocommerce-customize-store-sidebar__title {
|
||||||
|
color: #1e1e1e;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
line-height: 1;
|
||||||
|
padding-right: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: inherit;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
padding: 0 1rem;
|
||||||
|
color: #757575;
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-main {
|
||||||
|
margin-right: 2.5rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #2f2f2f;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-banner {
|
||||||
|
background: var(--woo-purple-woo-purple-0, #f2edff);
|
||||||
|
background-image: url(../assets/images/banner-design-with-ai.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: bottom right;
|
||||||
|
background-size: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
margin: 1.25rem 0 3.375rem;
|
||||||
|
min-height: 343px;
|
||||||
|
padding: 70px 0;
|
||||||
|
width: 820px;
|
||||||
|
|
||||||
|
.woocommerce-customize-store-banner-content {
|
||||||
|
width: 345px;
|
||||||
|
margin-left: 50px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.33;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1rem 0 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #3858e9;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #fff;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
padding: 10px 15px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #2234e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-theme-cards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 820px;
|
||||||
|
|
||||||
|
.theme-card {
|
||||||
|
flex-basis: 45%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e9e9e9;
|
||||||
|
width: 394px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-card__title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 1.5rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-customize-store-browse-themes {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #3858e9;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #3858e9;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
margin: 3.75rem 0;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,55 @@
|
||||||
export const fetchThemeCards = async () => {
|
export const fetchThemeCards = async () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
slug: 'twentytwentyone',
|
||||||
name: 'Twenty Twenty One',
|
name: 'Twenty Twenty One',
|
||||||
description: 'The default theme for WordPress.',
|
description: 'The default theme for WordPress.',
|
||||||
|
image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/pub/twentytwentyone/screenshot.png',
|
||||||
|
styleVariations: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
slug: 'twentytwenty',
|
||||||
name: 'Twenty Twenty',
|
name: 'Twenty Twenty',
|
||||||
description: 'The previous default theme for WordPress.',
|
description: 'The previous default theme for WordPress.',
|
||||||
|
image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/pub/twentytwenty/screenshot.png',
|
||||||
|
styleVariations: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'tsubaki',
|
||||||
|
name: 'Tsubaki',
|
||||||
|
description:
|
||||||
|
'Tsubaki puts the spotlight on your products and your customers. This theme leverages WooCommerce to provide you with intuitive product navigation and the patterns you need to master digital merchandising.',
|
||||||
|
image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/premium/tsubaki/screenshot.png',
|
||||||
|
styleVariations: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'winkel',
|
||||||
|
name: 'Winkel',
|
||||||
|
description:
|
||||||
|
'Winkel is a minimal, product-focused theme featuring Payments block. Its clean, cool look combined with a simple layout makes it perfect for showcasing fashion items – clothes, shoes, and accessories.',
|
||||||
|
image: 'https://i0.wp.com/s2.wp.com/wp-content/themes/pub/winkel/screenshot.png',
|
||||||
|
styleVariations: [
|
||||||
|
{
|
||||||
|
title: 'Default',
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#676767',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Charcoal',
|
||||||
|
primary: '#1f2527',
|
||||||
|
secondary: '#9fd3e8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Rainforest',
|
||||||
|
primary: '#eef4f7',
|
||||||
|
secondary: '#35845d',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Ruby Wine',
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#c8133e',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
export type ThemeCard = {
|
export type ThemeCard = {
|
||||||
// placeholder props, possibly take reference from https://github.com/Automattic/wp-calypso/blob/1f1b79210c49ef0d051f8966e24122229a334e29/packages/design-picker/src/components/theme-card/index.tsx#L32
|
// placeholder props, possibly take reference from https://github.com/Automattic/wp-calypso/blob/1f1b79210c49ef0d051f8966e24122229a334e29/packages/design-picker/src/components/theme-card/index.tsx#L32
|
||||||
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
isActive: boolean;
|
||||||
|
styleVariations: string[];
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,10 @@
|
||||||
body.woocommerce-customize-store.js.is-fullscreen-mode {
|
body.woocommerce-customize-store.js.is-fullscreen-mode {
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
& > div.tour-kit.woocommerce-tour-kit > div > div.tour-kit-spotlight.is-visible {
|
||||||
|
outline: 99999px solid rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-cys-layout {
|
.woocommerce-cys-layout {
|
||||||
|
@ -78,7 +82,7 @@ body.woocommerce-customize-store.js.is-fullscreen-mode {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
width: 404px;
|
width: 404px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: var(--gutenberg-transparent-blueberry, rgba(56, 88, 233, 0.04));
|
background: var(--wp-admin-theme-color-background-04, rgba(168, 168, 170, 0.301));
|
||||||
color: var(--gutenberg-gray-800, #2f2f2f);
|
color: var(--gutenberg-gray-800, #2f2f2f);
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { EmbeddedBodyLayout } from './embedded-body-layout';
|
||||||
import { WcAdminPaymentsGatewaysBannerSlot } from './payments/payments-settings-banner-slotfill';
|
import { WcAdminPaymentsGatewaysBannerSlot } from './payments/payments-settings-banner-slotfill';
|
||||||
import { WcAdminConflictErrorSlot } from './settings/conflict-error-slotfill.js';
|
import { WcAdminConflictErrorSlot } from './settings/conflict-error-slotfill.js';
|
||||||
import './xstate.js';
|
import './xstate.js';
|
||||||
|
import { deriveWpAdminBackgroundColours } from './utils/derive-wp-admin-background-colours';
|
||||||
|
|
||||||
// Modify webpack pubilcPath at runtime based on location of WordPress Plugin.
|
// Modify webpack pubilcPath at runtime based on location of WordPress Plugin.
|
||||||
// eslint-disable-next-line no-undef,camelcase
|
// eslint-disable-next-line no-undef,camelcase
|
||||||
|
@ -48,6 +49,8 @@ const embeddedRoot = document.getElementById( 'woocommerce-embedded-root' );
|
||||||
const settingsGroup = 'wc_admin';
|
const settingsGroup = 'wc_admin';
|
||||||
const hydrateUser = getAdminSetting( 'currentUserData' );
|
const hydrateUser = getAdminSetting( 'currentUserData' );
|
||||||
|
|
||||||
|
deriveWpAdminBackgroundColours();
|
||||||
|
|
||||||
if ( appRoot ) {
|
if ( appRoot ) {
|
||||||
let HydratedPageLayout = withSettingsHydration(
|
let HydratedPageLayout = withSettingsHydration(
|
||||||
settingsGroup,
|
settingsGroup,
|
||||||
|
|
|
@ -24,6 +24,10 @@
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
.components-snackbar__content-with-icon {
|
.components-snackbar__content-with-icon {
|
||||||
margin-left: 32px;
|
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">
|
<svg width="80" height="80" viewBox="0 0 80 80" 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"/>
|
<g clip-path="url(#clip0_2483_9556)">
|
||||||
<path d="M49.4449 18.7216H7.10547V22.5103H49.4449V18.7216Z" fill="#757575"/>
|
<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="M49.4449 30.2784H7.10547V34.067H49.4449V30.2784Z" 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="M49.4449 41.8351H7.10547V45.6237H49.4449V41.8351Z" 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="M66.8991 18.7216H56.9102V22.5103H66.8991V18.7216Z" fill="white"/>
|
<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="M66.8991 30.2783H56.9102V34.067H66.8991V30.2783Z" fill="white"/>
|
<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"/>
|
||||||
<path d="M66.8991 41.835H56.9102V45.6237H66.8991V41.835Z" fill="white"/>
|
</g>
|
||||||
<path d="M66.8993 63.9176H50.4043V71.1341H66.8993V63.9176Z" fill="#757575"/>
|
<defs>
|
||||||
<path d="M7.13379 55.5258H66.8714" stroke="#271B3D" stroke-width="0.510311" stroke-miterlimit="10"/>
|
<clipPath id="clip0_2483_9556">
|
||||||
<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"/>
|
<rect width="80" height="80" fill="white" transform="translate(0 80) rotate(-90)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
@ -4,11 +4,11 @@
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: $content-max-width;
|
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) {
|
@media screen and (min-width: $breakpoint-medium) {
|
||||||
.woocommerce-marketplace__content {
|
.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 <NoResults />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ProductListContent products={ products } />;
|
return (
|
||||||
|
<>
|
||||||
|
<CategorySelector />
|
||||||
|
<ProductListContent products={ products } />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -120,7 +125,6 @@ export default function Extensions(): JSX.Element {
|
||||||
<h2 className="woocommerce-marketplace__product-list-title woocommerce-marketplace__product-list-title--extensions">
|
<h2 className="woocommerce-marketplace__product-list-title woocommerce-marketplace__product-list-title--extensions">
|
||||||
{ title }
|
{ title }
|
||||||
</h2>
|
</h2>
|
||||||
<CategorySelector />
|
|
||||||
{ content() }
|
{ content() }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ import WooIcon from '../../assets/images/woo-icon.svg';
|
||||||
import { MARKETPLACE_HOST } from '../constants';
|
import { MARKETPLACE_HOST } from '../constants';
|
||||||
|
|
||||||
const refundPolicyTitle = createInterpolateElement(
|
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
|
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||||
a: <a href={ MARKETPLACE_HOST + '/refund-policy/' } />,
|
a: <a href={ MARKETPLACE_HOST + '/refund-policy/' } />,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
padding: $grid-unit-80 $grid-unit-40;
|
padding: $grid-unit-80 $grid-unit-40;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
margin-top: $grid-unit-30;
|
margin-top: $grid-unit-30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,12 +14,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-marketplace__no-results__icon {
|
.woocommerce-marketplace__no-results__icon {
|
||||||
height: 100px;
|
height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-marketplace__no-results__description {
|
.woocommerce-marketplace__no-results__description {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
max-width: 52ch;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: $gutenberg-gray-700;
|
color: $gutenberg-gray-700;
|
||||||
|
|
|
@ -90,13 +90,15 @@ export default function NoResults(): JSX.Element {
|
||||||
className="woocommerce-marketplace__no-results__icon"
|
className="woocommerce-marketplace__no-results__icon"
|
||||||
src={ NoResultsIcon }
|
src={ NoResultsIcon }
|
||||||
alt={ __( 'No results.', 'woocommerce' ) }
|
alt={ __( 'No results.', 'woocommerce' ) }
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
/>
|
/>
|
||||||
<div className="woocommerce-marketplace__no-results__description">
|
<div className="woocommerce-marketplace__no-results__description">
|
||||||
<h3 className="woocommerce-marketplace__no-results__description--bold">
|
<h3 className="woocommerce-marketplace__no-results__description--bold">
|
||||||
{ sprintf(
|
{ sprintf(
|
||||||
// translators: %s: search term
|
// translators: %s: search term
|
||||||
__(
|
__(
|
||||||
'We didn\'t find any results for "%s"',
|
"We didn't find any results for “%s”",
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
noResultsTerm
|
noResultsTerm
|
||||||
|
|
|
@ -22,12 +22,14 @@ export default function ProductListContent( props: {
|
||||||
title: product.title,
|
title: product.title,
|
||||||
icon: product.icon,
|
icon: product.icon,
|
||||||
vendorName: product.vendorName,
|
vendorName: product.vendorName,
|
||||||
vendorUrl: appendURLParams( product.vendorUrl, [
|
vendorUrl: product.vendorUrl
|
||||||
|
? appendURLParams( product.vendorUrl, [
|
||||||
[ 'utm_source', 'extensionsscreen' ],
|
[ 'utm_source', 'extensionsscreen' ],
|
||||||
[ 'utm_medium', 'product' ],
|
[ 'utm_medium', 'product' ],
|
||||||
[ 'utm_campaign', 'wcaddons' ],
|
[ 'utm_campaign', 'wcaddons' ],
|
||||||
[ 'utm_content', 'devpartner' ],
|
[ 'utm_content', 'devpartner' ],
|
||||||
] ),
|
] )
|
||||||
|
: '',
|
||||||
price: product.price,
|
price: product.price,
|
||||||
url: appendURLParams(
|
url: appendURLParams(
|
||||||
product.url,
|
product.url,
|
||||||
|
|
|
@ -3,11 +3,31 @@
|
||||||
.woocommerce-admin-page__extensions {
|
.woocommerce-admin-page__extensions {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
||||||
|
#wpbody-content {
|
||||||
|
/* Prevent double-scrollbar issue on WooCommerce > Extension pages */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-layout__primary {
|
.woocommerce-layout__primary {
|
||||||
margin: 0;
|
margin: $header-height-mobile 0 0;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin-top: $header-height-desktop;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-layout__main {
|
.woocommerce-layout__main {
|
||||||
padding: 0;
|
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 } =
|
const { isDirty, isValidForm, values, resetForm } =
|
||||||
useFormContext< Product >();
|
useFormContext< Product >();
|
||||||
|
|
||||||
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm );
|
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm() );
|
||||||
|
|
||||||
useCustomerEffortScoreExitPageTracker(
|
useCustomerEffortScoreExitPageTracker(
|
||||||
! values.id ? 'new_product' : 'editing_new_product',
|
! values.id ? 'new_product' : 'editing_new_product',
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue