Merge branch 'trunk' into feature/37127-marketing-reload-installed-extensions
This commit is contained in:
commit
186f1fb850
|
@ -0,0 +1,37 @@
|
|||
name: Run API tests
|
||||
description: Runs the WooCommerce Core API tests and generates Allure report.
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
report-name:
|
||||
description: Name of Allure report to be generated.
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Run API tests.
|
||||
id: run-api-tests
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
env:
|
||||
BASE_URL: http://localhost:8086
|
||||
USER_KEY: admin
|
||||
USER_SECRET: password
|
||||
run: pnpm exec playwright test --config=tests/api-core-tests/playwright.config.js
|
||||
|
||||
- name: Generate Test report.
|
||||
if: success() || ( failure() && steps.run-api-tests.conclusion == 'failure' )
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
|
||||
|
||||
- name: Archive test report
|
||||
if: success() || ( failure() && steps.run-api-tests.conclusion == 'failure' )
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ inputs.report-name }}
|
||||
path: |
|
||||
${{ env.ALLURE_RESULTS_DIR }}
|
||||
${{ env.ALLURE_REPORT_DIR }}
|
||||
retention-days: 20
|
|
@ -0,0 +1,41 @@
|
|||
name: Run E2E tests
|
||||
description: Runs the WooCommerce Core E2E tests and generates Allure report.
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
report-name:
|
||||
description: Name of Allure report to be generated.
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download and install Chromium browser.
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
- name: Run E2E tests.
|
||||
id: run-e2e-tests
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
USE_WP_ENV: 1
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
run: pnpm exec playwright test --config=tests/e2e-pw/playwright.config.js
|
||||
|
||||
- name: Generate Test report.
|
||||
if: success() || ( failure() && steps.run-e2e-tests.conclusion == 'failure' )
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
run: pnpm exec allure generate --clean ${{ env.ALLURE_RESULTS_DIR }} --output ${{ env.ALLURE_REPORT_DIR }}
|
||||
|
||||
- name: Archive test report
|
||||
if: success() || ( failure() && steps.run-e2e-tests.conclusion == 'failure' )
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ inputs.report-name }}
|
||||
path: |
|
||||
${{ env.ALLURE_RESULTS_DIR }}
|
||||
${{ env.ALLURE_REPORT_DIR }}
|
||||
retention-days: 20
|
|
@ -0,0 +1,17 @@
|
|||
name: Run k6 performance tests
|
||||
description: Runs the WooCommerce Core k6 performance tests.
|
||||
permissions: {}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install k6
|
||||
shell: bash
|
||||
run: |
|
||||
curl https://github.com/grafana/k6/releases/download/v0.33.0/k6-v0.33.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1
|
||||
|
||||
- name: Run k6 performance tests
|
||||
id: run-k6-tests
|
||||
shell: bash
|
||||
run: |
|
||||
./k6 run plugins/woocommerce/tests/performance/tests/gh-action-pr-requests.js
|
|
@ -0,0 +1,29 @@
|
|||
name: Setup local test environment
|
||||
description: Set up a wp-env testing environment
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
test-type:
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- e2e
|
||||
- api
|
||||
- k6
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Load docker images and start containers for E2E or API tests
|
||||
if: ( inputs.test-type == 'e2e' ) || ( inputs.test-type == 'api' )
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
run: pnpm run env:test
|
||||
|
||||
- name: Load docker images and start containers for k6 performance tests
|
||||
if: inputs.test-type == 'k6'
|
||||
working-directory: plugins/woocommerce
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm env:dev --filter=woocommerce
|
||||
pnpm env:performance-init --filter=woocommerce
|
|
@ -0,0 +1,41 @@
|
|||
name: Send Slack alert on PR merge test failure
|
||||
description: Send a Slack alert when automated tests failed on trunk after PR merge.
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
slack-bot-token:
|
||||
required: true
|
||||
channel-id:
|
||||
required: true
|
||||
test-type:
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- E2E
|
||||
- API
|
||||
- k6
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Compose Slack message
|
||||
id: compose-slack-message
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
TEST_TYPE: ${{ inputs.test-type }}
|
||||
with:
|
||||
script: |
|
||||
const script = require('./.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js')
|
||||
const slackMessage = script()
|
||||
core.setOutput('slack-message', slackMessage)
|
||||
|
||||
- name: Send Slack alert
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ inputs.slack-bot-token }}
|
||||
with:
|
||||
channel-id: ${{ inputs.channel-id }}
|
||||
payload: ${{ steps.compose-slack-message.outputs.slack-message }}
|
114
.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js
vendored
Normal file
114
.github/actions/tests/slack-alert-on-pr-merge/scripts/compose-slack-message.js
vendored
Normal file
|
@ -0,0 +1,114 @@
|
|||
module.exports = () => {
|
||||
const {
|
||||
GITHUB_BASE_REF,
|
||||
GITHUB_RUN_ID,
|
||||
PR_NUMBER,
|
||||
PR_TITLE,
|
||||
SHA,
|
||||
TEST_TYPE,
|
||||
} = process.env;
|
||||
|
||||
// Slack message blocks
|
||||
const blocks = [];
|
||||
const dividerBlock = {
|
||||
type: 'divider',
|
||||
};
|
||||
const introBlock = {
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `${ TEST_TYPE } tests failed on \`${ GITHUB_BASE_REF }\` after merging PR <https://github.com/woocommerce/woocommerce/pull/${ PR_NUMBER }|#${ PR_NUMBER }>`,
|
||||
},
|
||||
};
|
||||
const prTitleBlock = {
|
||||
type: 'header',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: PR_TITLE,
|
||||
emoji: true,
|
||||
},
|
||||
};
|
||||
const prButtonBlock = {
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View pull request :pr-merged:',
|
||||
emoji: true,
|
||||
},
|
||||
value: 'view_pr',
|
||||
url: `https://github.com/woocommerce/woocommerce/pull/${ PR_NUMBER }`,
|
||||
action_id: 'view-pr',
|
||||
},
|
||||
],
|
||||
};
|
||||
const mergeCommitBlock = {
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: `View merge commit ${ SHA.substring(
|
||||
0,
|
||||
7
|
||||
) } :alphabet-yellow-hash:`,
|
||||
emoji: true,
|
||||
},
|
||||
value: 'view_commit',
|
||||
url: `https://github.com/woocommerce/woocommerce/commit/${ SHA }`,
|
||||
action_id: 'view-commit',
|
||||
},
|
||||
],
|
||||
};
|
||||
const githubBlock = {
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View GitHub run log :github:',
|
||||
emoji: true,
|
||||
},
|
||||
value: 'view_github',
|
||||
url: `https://github.com/woocommerce/woocommerce/actions/runs/${ GITHUB_RUN_ID }`,
|
||||
action_id: 'view-github',
|
||||
},
|
||||
],
|
||||
};
|
||||
const reportBlock = {
|
||||
type: 'actions',
|
||||
elements: [
|
||||
{
|
||||
type: 'button',
|
||||
text: {
|
||||
type: 'plain_text',
|
||||
text: 'View test report :colorful-bar-chart:',
|
||||
emoji: true,
|
||||
},
|
||||
value: 'view_report',
|
||||
url: `https://woocommerce.github.io/woocommerce-test-reports/pr-merge/${ PR_NUMBER }/${ TEST_TYPE.toLowerCase() }`,
|
||||
action_id: 'view-report',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Assemble blocks
|
||||
blocks.push( dividerBlock );
|
||||
blocks.push( introBlock );
|
||||
blocks.push( prTitleBlock );
|
||||
blocks.push( prButtonBlock );
|
||||
blocks.push( mergeCommitBlock );
|
||||
blocks.push( githubBlock );
|
||||
|
||||
if ( [ 'e2e', 'api' ].includes( TEST_TYPE.toLowerCase() ) ) {
|
||||
blocks.push( reportBlock );
|
||||
}
|
||||
|
||||
blocks.push( dividerBlock );
|
||||
|
||||
return { blocks };
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
name: Upload Allure files to bucket
|
||||
description: Upload Allure files to bucket.
|
||||
permissions: {}
|
||||
|
||||
inputs:
|
||||
destination-dir:
|
||||
description: Directory under the "artifacts" S3 folder to which the Allure files would be uploaded.
|
||||
required: true
|
||||
aws-region:
|
||||
required: true
|
||||
aws-access-key-id:
|
||||
required: true
|
||||
aws-secret-access-key:
|
||||
required: true
|
||||
s3-bucket:
|
||||
required: true
|
||||
include-allure-results:
|
||||
dafault: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v1-node16
|
||||
with:
|
||||
aws-region: ${{ inputs.aws-region }}
|
||||
aws-access-key-id: ${{ inputs.aws-access-key-id }}
|
||||
aws-secret-access-key: ${{ inputs.aws-secret-access-key }}
|
||||
|
||||
- name: Upload 'allure-results' folder
|
||||
if: inputs.include-allure-results == true
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Uploading allure-results folder..."
|
||||
aws s3 sync ${{ env.ALLURE_RESULTS_DIR }} \
|
||||
${{ inputs.s3-bucket }}/artifacts/${{ github.run_id }}/${{ inputs.destination-dir }}/allure-results \
|
||||
--quiet
|
||||
echo "Done"
|
||||
|
||||
- name: Upload 'allure-report' folder
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Uploading allure-report folder..."
|
||||
aws s3 sync ${{ env.ALLURE_REPORT_DIR }} \
|
||||
${{ inputs.s3-bucket }}/artifacts/${{ github.run_id }}/${{ inputs.destination-dir }}/allure-report \
|
||||
--quiet
|
||||
echo "Done"
|
|
@ -65,3 +65,12 @@
|
|||
- plugins/woocommerce/src/Admin/**/*
|
||||
- plugins/woocommerce/src/Internal/Admin/**/*
|
||||
- plugins/woocommerce-admin/**/*
|
||||
|
||||
'focus: performance tests [team:Solaris]':
|
||||
- plugins/woocommerce/tests/performance/**/*
|
||||
|
||||
'focus: api tests [team:Solaris]':
|
||||
- plugins/woocommerce/tests/api-core-tests/**/*
|
||||
|
||||
'focus: e2e tests [team:Solaris]':
|
||||
- plugins/woocommerce/tests/e2e-pw/**/*
|
||||
|
|
|
@ -15,12 +15,14 @@ permissions: {}
|
|||
jobs:
|
||||
test:
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
||||
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }}
|
||||
name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} ${{ matrix.hpos && 'HPOS' || '' }}
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
continue-on-error: ${{ matrix.wp == 'nightly' }}
|
||||
env:
|
||||
HPOS: ${{ matrix.hpos }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
@ -33,6 +35,9 @@ jobs:
|
|||
php: 7.4
|
||||
- wp: '5.9'
|
||||
php: 7.4
|
||||
- wp: 'latest'
|
||||
php: '7.4'
|
||||
hpos: true
|
||||
services:
|
||||
database:
|
||||
image: mysql:5.6
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
name: Run tests against trunk after PR merge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
api:
|
||||
name: Run API tests
|
||||
runs-on: ubuntu-20.04
|
||||
if: (github.event.pull_request.merged == true) && (github.event.pull_request.base.ref == 'trunk')
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/api-core-tests/test-results/allure-report
|
||||
ARTIFACT_NAME: api-pr-merge-${{ github.event.pull_request.number }}-run-${{ github.run_number }}
|
||||
steps:
|
||||
- name: Checkout merge commit on trunk
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Setup local test environment
|
||||
uses: ./.github/actions/tests/setup-local-test-environment
|
||||
with:
|
||||
test-type: api
|
||||
|
||||
- name: Run API tests
|
||||
id: run-api-composite-action
|
||||
uses: ./.github/actions/tests/run-api-tests
|
||||
with:
|
||||
report-name: ${{ env.ARTIFACT_NAME }}
|
||||
|
||||
- name: Upload Allure files to bucket
|
||||
if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' )
|
||||
uses: ./.github/actions/tests/upload-allure-files-to-bucket
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }}
|
||||
aws-region: ${{ secrets.REPORTS_AWS_REGION }}
|
||||
aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }}
|
||||
destination-dir: ${{ env.ARTIFACT_NAME }}
|
||||
s3-bucket: ${{ secrets.REPORTS_BUCKET }}
|
||||
|
||||
- name: Publish Allure report
|
||||
if: success() || ( failure() && steps.run-api-composite-action.conclusion == 'failure' )
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
|
||||
run: |
|
||||
gh workflow run publish-test-reports-trunk-merge.yml \
|
||||
-f run_id=${{ github.run_id }} \
|
||||
-f artifact=${{ env.ARTIFACT_NAME }} \
|
||||
-f pr_number=${{ github.event.pull_request.number }} \
|
||||
-f test_type="api" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Send Slack alert on test failure
|
||||
if: failure() && steps.run-api-composite-action.conclusion == 'failure'
|
||||
uses: ./.github/actions/tests/slack-alert-on-pr-merge
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.E2E_SLACK_TOKEN }}
|
||||
channel-id: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }}
|
||||
test-type: API
|
||||
|
||||
e2e:
|
||||
name: Run E2E tests
|
||||
needs: [api]
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
ALLURE_RESULTS_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-results
|
||||
ALLURE_REPORT_DIR: ${{ github.workspace }}/plugins/woocommerce/tests/e2e-pw/test-results/allure-report
|
||||
ARTIFACT_NAME: e2e-pr-merge-${{ github.event.pull_request.number }}-run-${{ github.run_number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
with:
|
||||
build-filters: woocommerce
|
||||
|
||||
- name: Setup local test environment
|
||||
uses: ./.github/actions/tests/setup-local-test-environment
|
||||
with:
|
||||
test-type: e2e
|
||||
|
||||
- name: Run E2E tests
|
||||
id: run-e2e-composite-action
|
||||
timeout-minutes: 60
|
||||
uses: ./.github/actions/tests/run-e2e-tests
|
||||
env:
|
||||
E2E_MAX_FAILURES: 15
|
||||
with:
|
||||
report-name: ${{ env.ARTIFACT_NAME }}
|
||||
|
||||
- name: Upload Allure files to bucket
|
||||
if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' )
|
||||
uses: ./.github/actions/tests/upload-allure-files-to-bucket
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.REPORTS_AWS_ACCESS_KEY_ID }}
|
||||
aws-region: ${{ secrets.REPORTS_AWS_REGION }}
|
||||
aws-secret-access-key: ${{ secrets.REPORTS_AWS_SECRET_ACCESS_KEY }}
|
||||
destination-dir: ${{ env.ARTIFACT_NAME }}
|
||||
s3-bucket: ${{ secrets.REPORTS_BUCKET }}
|
||||
include-allure-results: false
|
||||
|
||||
- name: Publish Allure report
|
||||
if: success() || ( failure() && steps.run-e2e-composite-action.conclusion == 'failure' )
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPORTS_TOKEN }}
|
||||
run: |
|
||||
gh workflow run publish-test-reports-trunk-merge.yml \
|
||||
-f run_id=${{ github.run_id }} \
|
||||
-f artifact=${{ env.ARTIFACT_NAME }} \
|
||||
-f pr_number=${{ github.event.pull_request.number }} \
|
||||
-f test_type="e2e" \
|
||||
--repo woocommerce/woocommerce-test-reports
|
||||
|
||||
- name: Send Slack alert on test failure
|
||||
if: failure() && steps.run-e2e-composite-action.conclusion == 'failure'
|
||||
uses: ./.github/actions/tests/slack-alert-on-pr-merge
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.E2E_SLACK_TOKEN }}
|
||||
channel-id: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }}
|
||||
test-type: E2E
|
||||
|
||||
k6:
|
||||
name: Run k6 Performance tests
|
||||
needs: [api]
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
|
||||
- name: Setup WooCommerce Monorepo
|
||||
uses: ./.github/actions/setup-woocommerce-monorepo
|
||||
|
||||
- name: Setup local test environment
|
||||
uses: ./.github/actions/tests/setup-local-test-environment
|
||||
with:
|
||||
test-type: k6
|
||||
|
||||
- name: Run k6 performance tests
|
||||
id: run-k6-composite-action
|
||||
uses: './.github/actions/tests/run-k6-tests'
|
||||
|
||||
- name: Send Slack alert on test failure
|
||||
if: failure() && steps.run-k6-composite-action.conclusion == 'failure'
|
||||
uses: ./.github/actions/tests/slack-alert-on-pr-merge
|
||||
with:
|
||||
slack-bot-token: ${{ secrets.E2E_SLACK_TOKEN }}
|
||||
channel-id: ${{ secrets.E2E_TRUNK_SLACK_CHANNEL }}
|
||||
test-type: k6
|
|
@ -1,5 +1,78 @@
|
|||
== Changelog ==
|
||||
|
||||
= 7.5.1 2023-03-21 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Fix - Fix no enforcing of min/max limits in quantity selector of variable products. [#36871](https://github.com/woocommerce/woocommerce/pull/36871)
|
||||
* Dev - Update column definitions with synonymous types to prevent dbDelta from trying to ALTER them on each install. [#37277](https://github.com/woocommerce/woocommerce/pull/37277)
|
||||
* Update - Update WooCommerce Blocks to 9.6.6. [#37298](https://github.com/woocommerce/woocommerce/pull/37298)
|
||||
|
||||
= 7.5.0 2023-03-14 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Fix - Add HPOS support to the reserved stock query [#36535](https://github.com/woocommerce/woocommerce/pull/36535)
|
||||
* Fix - Comment: Fix inconsistencies on Analytics > Orders table when using date_paid or date_completed [#36876](https://github.com/woocommerce/woocommerce/pull/36876)
|
||||
* Fix - Define a public `api` property in the WooCommerce class to prevent a PHP deprecation warning [#36545](https://github.com/woocommerce/woocommerce/pull/36545)
|
||||
* Fix - Don't delete order from posts table when deleted from orders table if the later is authoritative and sync is off [#36617](https://github.com/woocommerce/woocommerce/pull/36617)
|
||||
* Fix - Eliminate data store internal meta keys duplicates [#36611](https://github.com/woocommerce/woocommerce/pull/36611)
|
||||
* Fix - Ensure changes made via the `woocommerce_order_list_table_prepare_items_query_args` are observed. [#36649](https://github.com/woocommerce/woocommerce/pull/36649)
|
||||
* Fix - Ensuring that we know if allowTracking is true before adding exit page. [#36656](https://github.com/woocommerce/woocommerce/pull/36656)
|
||||
* Fix - Fix Ampersand changed to & on product attribute export [#36525](https://github.com/woocommerce/woocommerce/pull/36525)
|
||||
* Fix - Fix decimal points for NOK currency [#36780](https://github.com/woocommerce/woocommerce/pull/36780)
|
||||
* Fix - Fix inconsitent product task icon colors [#36889](https://github.com/woocommerce/woocommerce/pull/36889)
|
||||
* Fix - Fix WordPress unit tests libraries being installed in a symlinked folder structure [#36641](https://github.com/woocommerce/woocommerce/pull/36641)
|
||||
* Fix - Make states optional for Hungary and Bulgaria. [#36701](https://github.com/woocommerce/woocommerce/pull/36701)
|
||||
* Fix - Screen ID matching switched to untranslated 'woocommerce' strings. [#36854](https://github.com/woocommerce/woocommerce/pull/36854)
|
||||
* Fix - Translate the labels for units of measure. [#36708](https://github.com/woocommerce/woocommerce/pull/36708)
|
||||
* Fix - Update `config@3.3.7` (from `3.3.3`). Fix `node_env_var_name is not defined` error. [#33828](https://github.com/woocommerce/woocommerce/pull/33828)
|
||||
* Add - Add 'add_tab' method in FormFactory to allow plugins to extend the WooCommerce admin product form [#36583](https://github.com/woocommerce/woocommerce/pull/36583)
|
||||
* Add - Add @woocommerce/product-editor dependency and change dependency of ProductSectionLayout component. [#36600](https://github.com/woocommerce/woocommerce/pull/36600)
|
||||
* Add - Add additional global attributes and local attributes information when saving product attributes [#36858](https://github.com/woocommerce/woocommerce/pull/36858)
|
||||
* Add - Add a new Channels card in multichannel marketing page. [#36541](https://github.com/woocommerce/woocommerce/pull/36541)
|
||||
* Add - Add an experimental slot for marketing overview extensibility [#36828](https://github.com/woocommerce/woocommerce/pull/36828)
|
||||
* Add - Add slot fill support for tabs for the new product management MVP. [#36551](https://github.com/woocommerce/woocommerce/pull/36551)
|
||||
* Add - Add survey after disabling new experience [#36544](https://github.com/woocommerce/woocommerce/pull/36544)
|
||||
* Add - Add unique sku option to error data when setting product sku [#36612](https://github.com/woocommerce/woocommerce/pull/36612)
|
||||
* Add - Add WC-specific criteria to the Site Health test for persistent object caches [#35202](https://github.com/woocommerce/woocommerce/pull/35202)
|
||||
* Add - Enable new experience when new user selects "Physical product". [#36406](https://github.com/woocommerce/woocommerce/pull/36406)
|
||||
* Update - Update WooCommerce Blocks to 9.6.5 [#37051](https://github.com/woocommerce/woocommerce/pull/37051)
|
||||
* Update - Update WooCommerce Blocks to 9.6.3 [#36992](https://github.com/woocommerce/woocommerce/pull/36992)
|
||||
* Update - Update WooCommerce Blocks to 9.6.2 [#36919](https://github.com/woocommerce/woocommerce/pull/36919)
|
||||
* Update - Add date_paid and date_completed date sorting options for Revenue and Order reports [#36492](https://github.com/woocommerce/woocommerce/pull/36492)
|
||||
* Update - Add default value for backorders [#36607](https://github.com/woocommerce/woocommerce/pull/36607)
|
||||
* Update - Add Skydropx, Envia, Sendcloud, Packlink to shipping task [#36873](https://github.com/woocommerce/woocommerce/pull/36873)
|
||||
* Update - Always show comments for product feedback form [#36484](https://github.com/woocommerce/woocommerce/pull/36484)
|
||||
* Update - Delete FlexSlider code for legacy browsers. [#36690](https://github.com/woocommerce/woocommerce/pull/36690)
|
||||
* Update - Disable the new product editor, pending design updates. [#36894](https://github.com/woocommerce/woocommerce/pull/36894)
|
||||
* Update - Have "Grow your store" appear first in marketing task by default [#36826](https://github.com/woocommerce/woocommerce/pull/36826)
|
||||
* Update - Migrating product editor pricing section to slot fills. [#36500](https://github.com/woocommerce/woocommerce/pull/36500)
|
||||
* Update - Refactor slot fills to ensure variant fills have distinct slots. [#36646](https://github.com/woocommerce/woocommerce/pull/36646)
|
||||
* Update - Removed I.D column from product import samples [#36857](https://github.com/woocommerce/woocommerce/pull/36857)
|
||||
* Update - Remove Meta from grow your store list [#36886](https://github.com/woocommerce/woocommerce/pull/36886)
|
||||
* Update - Remove opinionated styles from buttons in block themes so they inherit theme styles more accurately [#36651](https://github.com/woocommerce/woocommerce/pull/36651)
|
||||
* Update - Replace $.ajax() calls with browser-native window.fetch() calls. [#36275](https://github.com/woocommerce/woocommerce/pull/36275)
|
||||
* Update - Update payment gateway list ordering priority and remove Klarna from North America [#36550](https://github.com/woocommerce/woocommerce/pull/36550)
|
||||
* Update - Update Playwright version from 1.28.0 -> 1.30.0 [#36789](https://github.com/woocommerce/woocommerce/pull/36789)
|
||||
* Update - Updates to product editor fill to support new prop API. [#36592](https://github.com/woocommerce/woocommerce/pull/36592)
|
||||
* Update - Update WooCommerce Blocks 9.6.0 & 9.6.1 [#36852](https://github.com/woocommerce/woocommerce/pull/36852)
|
||||
* Dev - Add attribute creation form when there are no attributes [#36606](https://github.com/woocommerce/woocommerce/pull/36606)
|
||||
* Dev - Add a unit test for woocommerce_admin_experimental_onboarding_tasklists filter [#36827](https://github.com/woocommerce/woocommerce/pull/36827)
|
||||
* Dev - Code refactor on marketing components. [#36540](https://github.com/woocommerce/woocommerce/pull/36540)
|
||||
* Dev - Made e2e selectors more robust [#36499](https://github.com/woocommerce/woocommerce/pull/36499)
|
||||
* Dev - Remove attribute type logic from attribute component [#36563](https://github.com/woocommerce/woocommerce/pull/36563)
|
||||
* Dev - Update eslint to 8.32.0 across the monorepo. [#36700](https://github.com/woocommerce/woocommerce/pull/36700)
|
||||
* Dev - Update pnpm command to run e2e tests for consistency. Also update docs with new command. [#35287](https://github.com/woocommerce/woocommerce/pull/35287)
|
||||
* Tweak - Add IR and fields priorities to list of get_country_locale() method to follow conventional way of addressing in Iran. [#36491](https://github.com/woocommerce/woocommerce/pull/36491)
|
||||
* Tweak - Add missing deprecation notice for filter hook woocommerce_my_account_my_orders_columns. [#36356](https://github.com/woocommerce/woocommerce/pull/36356)
|
||||
* Tweak - Adjust default sizes for the quantity and coupon input fields within the cart page. [#29122](https://github.com/woocommerce/woocommerce/pull/29122)
|
||||
* Tweak - Do not display low/out-of-stock information in the dashboard status widget when stock management is disabled. [#36703](https://github.com/woocommerce/woocommerce/pull/36703)
|
||||
* Tweak - Remove free trial terms from Avalara tax task [#36888](https://github.com/woocommerce/woocommerce/pull/36888)
|
||||
* Tweak - Tweak product link description and display in the new product management experience [#36591](https://github.com/woocommerce/woocommerce/pull/36591)
|
||||
* Enhancement - Change the sass variable names to more predictable ones. [#28908](https://github.com/woocommerce/woocommerce/pull/28908)
|
||||
|
||||
|
||||
= 7.4.1 2023-03-01 =
|
||||
|
||||
**WooCommerce**
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding simple DisplayState wrapper and modifying Collapsible component to allow rendering hidden content.
|
|
@ -7,10 +7,12 @@ import { Icon, chevronDown, chevronUp } from '@wordpress/icons';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { DisplayState } from '../display-state';
|
||||
|
||||
export type CollapsedProps = {
|
||||
initialCollapsed?: boolean;
|
||||
toggleText: string;
|
||||
persistRender?: boolean;
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes< HTMLDivElement >;
|
||||
|
||||
|
@ -18,9 +20,19 @@ export const CollapsibleContent: React.FC< CollapsedProps > = ( {
|
|||
initialCollapsed = true,
|
||||
toggleText,
|
||||
children,
|
||||
persistRender = false,
|
||||
...props
|
||||
}: CollapsedProps ) => {
|
||||
const [ collapsed, setCollapsed ] = useState( initialCollapsed );
|
||||
|
||||
const getState = () => {
|
||||
if ( ! collapsed ) {
|
||||
return 'visible';
|
||||
}
|
||||
|
||||
return persistRender ? 'visually-hidden' : 'hidden';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-expanded={ collapsed ? 'false' : 'true' }
|
||||
|
@ -38,14 +50,14 @@ export const CollapsibleContent: React.FC< CollapsedProps > = ( {
|
|||
/>
|
||||
</div>
|
||||
</button>
|
||||
{ ! collapsed && (
|
||||
<DisplayState state={ getState() }>
|
||||
<div
|
||||
{ ...props }
|
||||
className="woocommerce-collapsible-content__content"
|
||||
>
|
||||
{ children }
|
||||
</div>
|
||||
) }
|
||||
</DisplayState>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
export type DisplayStateProps = {
|
||||
state?: 'visible' | 'visually-hidden' | 'hidden';
|
||||
children: React.ReactNode;
|
||||
} & React.HTMLAttributes< HTMLDivElement >;
|
||||
|
||||
export const DisplayState: React.FC< DisplayStateProps > = ( {
|
||||
state = 'visible',
|
||||
children,
|
||||
} ) => {
|
||||
if ( state === 'visible' ) {
|
||||
return <>{ children }</>;
|
||||
}
|
||||
|
||||
if ( state === 'visually-hidden' ) {
|
||||
return <div style={ { display: 'none' } }>{ children }</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './display-state';
|
|
@ -103,3 +103,4 @@ export {
|
|||
ProductSectionLayout as __experimentalProductSectionLayout,
|
||||
ProductFieldSection as __experimentalProductFieldSection,
|
||||
} from './product-section-layout';
|
||||
export { DisplayState } from './display-state';
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Move additional components to @woocommerce/customer-effort-score.
|
|
@ -51,6 +51,7 @@
|
|||
"@woocommerce/eslint-plugin": "workspace:*",
|
||||
"@woocommerce/internal-style-build": "workspace:*",
|
||||
"@woocommerce/navigation": "workspace:*",
|
||||
"@woocommerce/tracks": "workspace:*",
|
||||
"@wordpress/browserslist-config": "wp-6.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"css-loader": "^3.6.0",
|
||||
|
|
|
@ -3,18 +3,17 @@
|
|||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import {
|
||||
CustomerFeedbackModal,
|
||||
STORE_KEY,
|
||||
} from '@woocommerce/customer-effort-score';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getStoreAgeInWeeks } from './utils';
|
||||
import { ADMIN_INSTALL_TIMESTAMP_OPTION_NAME } from './constants';
|
||||
import { CustomerFeedbackModal } from '../';
|
||||
import { getStoreAgeInWeeks } from '../../utils';
|
||||
import { STORE_KEY } from '../../store';
|
||||
import { ADMIN_INSTALL_TIMESTAMP_OPTION_NAME } from '../../constants';
|
||||
|
||||
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
||||
'woocommerce_ces_product_mvp_ces_action';
|
|
@ -4,17 +4,15 @@
|
|||
import { useEffect } from 'react';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import {
|
||||
QUEUE_OPTION_NAME,
|
||||
STORE_KEY,
|
||||
} from '@woocommerce/customer-effort-score';
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CustomerEffortScoreTracks from './customer-effort-score-tracks';
|
||||
import { CustomerEffortScoreTracks } from '../';
|
||||
import { QUEUE_OPTION_NAME, STORE_KEY } from '../../store';
|
||||
|
||||
/**
|
||||
* Maps the queue of CES tracks surveys to CustomerEffortScoreTracks
|
||||
|
@ -27,7 +25,7 @@ import CustomerEffortScoreTracks from './customer-effort-score-tracks';
|
|||
* @param {boolean} props.resolving Whether the queue is resolving.
|
||||
* @param {Function} props.clearQueue Sets up clearing of the queue on the next page load.
|
||||
*/
|
||||
function CustomerEffortScoreTracksContainer( {
|
||||
function _CustomerEffortScoreTracksContainer( {
|
||||
queue,
|
||||
resolving,
|
||||
clearQueue,
|
||||
|
@ -67,7 +65,7 @@ function CustomerEffortScoreTracksContainer( {
|
|||
);
|
||||
}
|
||||
|
||||
CustomerEffortScoreTracksContainer.propTypes = {
|
||||
_CustomerEffortScoreTracksContainer.propTypes = {
|
||||
/**
|
||||
* The queue of CES tracks surveys to display.
|
||||
*/
|
||||
|
@ -82,7 +80,7 @@ CustomerEffortScoreTracksContainer.propTypes = {
|
|||
clearQueue: PropTypes.func,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
export const CustomerEffortScoreTracksContainer = compose(
|
||||
withSelect( ( select ) => {
|
||||
const { getCesSurveyQueue, isResolving } = select( STORE_KEY );
|
||||
const queue = getCesSurveyQueue();
|
||||
|
@ -109,4 +107,4 @@ export default compose(
|
|||
},
|
||||
};
|
||||
} )
|
||||
)( CustomerEffortScoreTracksContainer );
|
||||
)( _CustomerEffortScoreTracksContainer );
|
|
@ -1,26 +1,24 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
ALLOW_TRACKING_OPTION_NAME,
|
||||
CustomerEffortScore,
|
||||
} from '@woocommerce/customer-effort-score';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { withSelect, withDispatch } from '@wordpress/data';
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CustomerEffortScore } from '../';
|
||||
import {
|
||||
SHOWN_FOR_ACTIONS_OPTION_NAME,
|
||||
ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
|
||||
} from './constants';
|
||||
import { getStoreAgeInWeeks } from './utils';
|
||||
ALLOW_TRACKING_OPTION_NAME,
|
||||
SHOWN_FOR_ACTIONS_OPTION_NAME,
|
||||
} from '../../constants';
|
||||
import { getStoreAgeInWeeks } from '../../utils';
|
||||
|
||||
/**
|
||||
* A CustomerEffortScore wrapper that uses tracks to track the selected
|
||||
|
@ -43,7 +41,7 @@ import { getStoreAgeInWeeks } from './utils';
|
|||
* @param {Function} props.updateOptions Function to update options.
|
||||
* @param {Function} props.createNotice Function to create a snackbar.
|
||||
*/
|
||||
function CustomerEffortScoreTracks( {
|
||||
function _CustomerEffortScoreTracks( {
|
||||
action,
|
||||
trackProps,
|
||||
title,
|
||||
|
@ -176,7 +174,7 @@ function CustomerEffortScoreTracks( {
|
|||
);
|
||||
}
|
||||
|
||||
CustomerEffortScoreTracks.propTypes = {
|
||||
_CustomerEffortScoreTracks.propTypes = {
|
||||
/**
|
||||
* The action name sent to Tracks.
|
||||
*/
|
||||
|
@ -219,7 +217,7 @@ CustomerEffortScoreTracks.propTypes = {
|
|||
createNotice: PropTypes.func,
|
||||
};
|
||||
|
||||
export default compose(
|
||||
export const CustomerEffortScoreTracks = compose(
|
||||
withSelect( ( select ) => {
|
||||
const { getOption, hasFinishedResolution } =
|
||||
select( OPTIONS_STORE_NAME );
|
||||
|
@ -262,4 +260,4 @@ export default compose(
|
|||
createNotice,
|
||||
};
|
||||
} )
|
||||
)( CustomerEffortScoreTracks );
|
||||
)( _CustomerEffortScoreTracks );
|
|
@ -0,0 +1,8 @@
|
|||
export * from './customer-effort-score';
|
||||
export * from './customer-effort-score-modal-container';
|
||||
export * from './customer-effort-score-tracks';
|
||||
export * from './customer-effort-score-tracks-container';
|
||||
export * from './customer-feedback-simple';
|
||||
export * from './customer-feedback-modal';
|
||||
export * from './product-mvp-feedback-modal';
|
||||
export * from './feedback-modal';
|
|
@ -1 +1,7 @@
|
|||
export const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
||||
'woocommerce_admin_install_timestamp';
|
||||
|
||||
export const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
|
||||
|
||||
export const SHOWN_FOR_ACTIONS_OPTION_NAME =
|
||||
'woocommerce_ces_shown_for_actions';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './use-customer-effort-score-exit-page-tracker';
|
|
@ -1,9 +1,5 @@
|
|||
export * from './components/customer-effort-score';
|
||||
export * from './components/customer-feedback-simple';
|
||||
export * from './components/customer-feedback-modal';
|
||||
export * from './components/product-mvp-feedback-modal';
|
||||
export * from './components/feedback-modal';
|
||||
export * from './hooks/use-customer-effort-score-exit-page-tracker';
|
||||
export * from './store';
|
||||
export * from './utils/customer-effort-score-exit-page';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
export * from './hooks';
|
||||
export * from './store';
|
||||
export * from './utils';
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './customer-effort-score-exit-page';
|
||||
export * from './get-store-age-in-weeks';
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Fix linter errors
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: enhancement
|
||||
|
||||
Add tracks for plugin actions and handle plugin error properly
|
|
@ -28,6 +28,7 @@
|
|||
"dependencies": {
|
||||
"@woocommerce/date": "workspace:*",
|
||||
"@woocommerce/navigation": "workspace:*",
|
||||
"@woocommerce/tracks": "workspace:*",
|
||||
"@wordpress/api-fetch": "wp-6.0",
|
||||
"@wordpress/compose": "wp-6.0",
|
||||
"@wordpress/core-data": "wp-6.0",
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import { DispatchFromMap } from '@automattic/data-stores';
|
||||
import { controls } from '@wordpress/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -49,21 +50,22 @@ const isPluginResponseError = (
|
|||
typeof error === 'object' && error !== null && plugins[ 0 ] in error;
|
||||
|
||||
const formatErrorMessage = (
|
||||
pluginErrors: PluginResponseErrors,
|
||||
actionType = 'install'
|
||||
actionType: 'install' | 'activate' = 'install',
|
||||
plugins: Partial< PluginNames >[],
|
||||
rawErrorMessage: string
|
||||
) => {
|
||||
return sprintf(
|
||||
/* translators: %(actionType): install or activate (the plugin). %(pluginName): a plugin slug (e.g. woocommerce-services). %(error): a single error message or in plural a comma separated error message list.*/
|
||||
_n(
|
||||
'Could not %(actionType)s %(pluginName)s plugin, %(error)s',
|
||||
'Could not %(actionType)s the following plugins: %(pluginName)s with these Errors: %(error)s',
|
||||
Object.keys( pluginErrors ).length || 1,
|
||||
Object.keys( plugins ).length || 1,
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
actionType,
|
||||
pluginName: Object.keys( pluginErrors ).join( ', ' ),
|
||||
error: Object.values( pluginErrors ).join( ', \n' ),
|
||||
pluginName: plugins.join( ', ' ),
|
||||
error: rawErrorMessage,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -174,35 +176,42 @@ export function setRecommendedPlugins(
|
|||
}
|
||||
|
||||
function* handlePluginAPIError(
|
||||
actionType: string,
|
||||
actionType: 'install' | 'activate',
|
||||
plugins: Partial< PluginNames >[],
|
||||
error: unknown
|
||||
) {
|
||||
yield setError( 'installPlugins', error );
|
||||
let rawErrorMessage;
|
||||
|
||||
let pluginResponseError = error;
|
||||
if (
|
||||
( error instanceof Error || isRestApiError( error ) ) &&
|
||||
plugins[ 0 ]
|
||||
) {
|
||||
pluginResponseError = {
|
||||
[ plugins[ 0 ] ]: [ error.message ],
|
||||
};
|
||||
}
|
||||
|
||||
if ( isPluginResponseError( plugins, pluginResponseError ) ) {
|
||||
throw new PluginError(
|
||||
formatErrorMessage( pluginResponseError, actionType ),
|
||||
pluginResponseError
|
||||
);
|
||||
if ( isPluginResponseError( plugins, error ) ) {
|
||||
// Backend error messages are in the form of { plugin-slug: [ error messages ] }.
|
||||
rawErrorMessage = Object.values( error ).join( ', \n' );
|
||||
} else {
|
||||
throw new PluginError(
|
||||
`Unexpected Plugin Error: ${ JSON.stringify(
|
||||
pluginResponseError
|
||||
) }`,
|
||||
pluginResponseError
|
||||
);
|
||||
// Other error such as API connection errors.
|
||||
rawErrorMessage =
|
||||
isRestApiError( error ) || error instanceof Error
|
||||
? error.message
|
||||
: JSON.stringify( error );
|
||||
}
|
||||
|
||||
// Track the error.
|
||||
switch ( actionType ) {
|
||||
case 'install':
|
||||
recordEvent( 'install_plugins_error', {
|
||||
plugins: plugins.join( ', ' ),
|
||||
message: rawErrorMessage,
|
||||
} );
|
||||
break;
|
||||
case 'activate':
|
||||
recordEvent( 'activate_plugins_error', {
|
||||
plugins: plugins.join( ', ' ),
|
||||
message: rawErrorMessage,
|
||||
} );
|
||||
}
|
||||
|
||||
throw new PluginError(
|
||||
formatErrorMessage( actionType, plugins, rawErrorMessage ),
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
// Action Creator Generators
|
||||
|
@ -225,6 +234,7 @@ export function* installPlugins( plugins: Partial< PluginNames >[] ) {
|
|||
|
||||
return results;
|
||||
} catch ( error ) {
|
||||
yield setError( 'installPlugins', error );
|
||||
yield handlePluginAPIError( 'install', plugins, error );
|
||||
} finally {
|
||||
yield setIsRequesting( 'installPlugins', false );
|
||||
|
@ -251,6 +261,7 @@ export function* activatePlugins( plugins: Partial< PluginNames >[] ) {
|
|||
|
||||
return results;
|
||||
} catch ( error ) {
|
||||
yield setError( 'activatePlugins', error );
|
||||
yield handlePluginAPIError( 'activate', plugins, error );
|
||||
} finally {
|
||||
yield setIsRequesting( 'activatePlugins', false );
|
||||
|
@ -305,7 +316,7 @@ export function* connectToJetpack(
|
|||
}
|
||||
|
||||
export function* installJetpackAndConnect(
|
||||
errorAction: ( errorMesage: string ) => void,
|
||||
errorAction: ( errorMessage: string ) => void,
|
||||
getAdminLink: ( endpoint: string ) => string
|
||||
) {
|
||||
try {
|
||||
|
@ -329,7 +340,7 @@ export function* installJetpackAndConnect(
|
|||
|
||||
export function* connectToJetpackWithFailureRedirect(
|
||||
failureRedirect: string,
|
||||
errorAction: ( errorMesage: string ) => void,
|
||||
errorAction: ( errorMessage: string ) => void,
|
||||
getAdminLink: ( endpoint: string ) => string
|
||||
) {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: '../internal-js-tests/babel.config.js',
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add custom validation hook
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add new pricing block to the product editor package.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add summary block
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding Collapsible block with support for flexible rendering.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Fix issue were template was not re-synced when switching between products.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"rootDir": "./src",
|
||||
"preset": "../../internal-js-tests/jest.config.js"
|
||||
}
|
|
@ -36,6 +36,7 @@
|
|||
"@woocommerce/data": "workspace:^4.1.0",
|
||||
"@woocommerce/navigation": "workspace:^8.1.0",
|
||||
"@woocommerce/number": "workspace:*",
|
||||
"@woocommerce/settings": "^1.0.0",
|
||||
"@woocommerce/tracks": "workspace:^1.3.0",
|
||||
"@wordpress/block-editor": "^9.8.0",
|
||||
"@wordpress/blocks": "^12.3.0",
|
||||
|
@ -57,8 +58,15 @@
|
|||
"react-router-dom": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/testing-library__jest-dom": "^5.14.3",
|
||||
"@types/wordpress__block-editor": "^7.0.0",
|
||||
"@types/wordpress__block-library": "^2.6.1",
|
||||
"@types/wordpress__blocks": "^11.0.7",
|
||||
|
@ -90,10 +98,12 @@
|
|||
},
|
||||
"scripts": {
|
||||
"turbo:build": "pnpm run build:js && pnpm run build:css",
|
||||
"turbo:test": "jest --config ./jest.config.json",
|
||||
"prepare": "composer install",
|
||||
"changelog": "composer exec -- changelogger",
|
||||
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
|
||||
"build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name",
|
||||
"test": "pnpm -w exec turbo run turbo:test --filter=$npm_package_name",
|
||||
"lint": "eslint src",
|
||||
"build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json",
|
||||
"build:css": "webpack",
|
||||
|
|
|
@ -94,7 +94,7 @@ export function BlockEditor( {
|
|||
synchronizeBlocksWithTemplate( [], _settings?.template ),
|
||||
{}
|
||||
);
|
||||
}, [] );
|
||||
}, [ product.id ] );
|
||||
|
||||
if ( ! blocks ) {
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/collapsible",
|
||||
"title": "Collapsible",
|
||||
"category": "widgets",
|
||||
"description": "Container with collapsible inner blocks.",
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"toggleText": {
|
||||
"type": "string"
|
||||
},
|
||||
"initialCollapsed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"persistRender": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": true,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CollapsibleContent } from '@woocommerce/components';
|
||||
import type { BlockAttributes } from '@wordpress/blocks';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
|
||||
|
||||
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { toggleText, initialCollapsed, persistRender = true } = attributes;
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<CollapsibleContent
|
||||
toggleText={ toggleText }
|
||||
initialCollapsed={ initialCollapsed }
|
||||
persistRender={ persistRender }
|
||||
>
|
||||
<InnerBlocks templateLock="all" />
|
||||
</CollapsibleContent>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { initBlock } from '../../utils';
|
||||
import metadata from './block.json';
|
||||
import { Edit } from './edit';
|
||||
|
||||
const { name } = metadata;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings = {
|
||||
example: {},
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
export const init = () =>
|
||||
initBlock( { name, metadata: metadata as never, settings } );
|
|
@ -14,4 +14,9 @@ export const settings = {
|
|||
edit: Edit,
|
||||
};
|
||||
|
||||
export const init = () => initBlock( { name, metadata, settings } );
|
||||
export const init = () =>
|
||||
initBlock( {
|
||||
name,
|
||||
metadata: metadata as never,
|
||||
settings,
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-summary",
|
||||
"title": "Product summary",
|
||||
"category": "widgets",
|
||||
"description": "The product summary.",
|
||||
"keywords": [ "products", "summary", "excerpt" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"align": {
|
||||
"type": "string"
|
||||
},
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"enum": [ "ltr", "rtl" ]
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
alignCenter,
|
||||
alignJustify,
|
||||
alignLeft,
|
||||
alignRight,
|
||||
} from '@wordpress/icons';
|
||||
|
||||
export const ALIGNMENT_CONTROLS = [
|
||||
{
|
||||
icon: alignLeft,
|
||||
title: __( 'Align text left', 'woocommerce' ),
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
icon: alignCenter,
|
||||
title: __( 'Align text center', 'woocommerce' ),
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
icon: alignRight,
|
||||
title: __( 'Align text right', 'woocommerce' ),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
icon: alignJustify,
|
||||
title: __( 'Align text justify', 'woocommerce' ),
|
||||
align: 'justify',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { BlockEditProps } from '@wordpress/blocks';
|
||||
import { BaseControl } from '@wordpress/components';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
AlignmentControl,
|
||||
BlockControls,
|
||||
RichText,
|
||||
useBlockProps,
|
||||
} from '@wordpress/block-editor';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ParagraphRTLControl } from './paragraph-rtl-control';
|
||||
import { SummaryAttributes } from './types';
|
||||
import { ALIGNMENT_CONTROLS } from './constants';
|
||||
|
||||
export function Edit( {
|
||||
attributes,
|
||||
setAttributes,
|
||||
}: BlockEditProps< SummaryAttributes > ) {
|
||||
const { align, direction, label } = attributes;
|
||||
const blockProps = useBlockProps( {
|
||||
style: { direction },
|
||||
} );
|
||||
const id = uniqueId();
|
||||
const [ summary, setSummary ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
'short_description'
|
||||
);
|
||||
|
||||
function handleAlignmentChange( value: SummaryAttributes[ 'align' ] ) {
|
||||
setAttributes( { align: value } );
|
||||
}
|
||||
|
||||
function handleDirectionChange( value: SummaryAttributes[ 'direction' ] ) {
|
||||
setAttributes( { direction: value } );
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||
{ /* @ts-ignore No types for this exist yet. */ }
|
||||
<BlockControls group="block">
|
||||
<AlignmentControl
|
||||
alignmentControls={ ALIGNMENT_CONTROLS }
|
||||
value={ align }
|
||||
onChange={ handleAlignmentChange }
|
||||
/>
|
||||
|
||||
<ParagraphRTLControl
|
||||
direction={ direction }
|
||||
onChange={ handleDirectionChange }
|
||||
/>
|
||||
</BlockControls>
|
||||
|
||||
<BaseControl
|
||||
id={ id }
|
||||
label={ label || __( 'Summary', 'woocommerce' ) }
|
||||
>
|
||||
<RichText
|
||||
id={ id }
|
||||
identifier="content"
|
||||
tagName="p"
|
||||
value={ summary }
|
||||
onChange={ setSummary }
|
||||
placeholder={ __(
|
||||
"Summarize this product in 1-2 short sentences. We'll show it at the top of the page.",
|
||||
'woocommerce'
|
||||
) }
|
||||
data-empty={ Boolean( summary ) }
|
||||
className={ classNames( 'components-summary-control', {
|
||||
[ `has-text-align-${ align }` ]: align,
|
||||
} ) }
|
||||
dir={ direction }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { initBlock } from '../../utils';
|
||||
import blockConfiguration from './block.json';
|
||||
import { Edit } from './edit';
|
||||
import { SummaryAttributes } from './types';
|
||||
|
||||
const { name, ...metadata } =
|
||||
blockConfiguration as BlockConfiguration< SummaryAttributes >;
|
||||
|
||||
export { name, metadata };
|
||||
|
||||
export const settings = {
|
||||
example: {},
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
export function init() {
|
||||
return initBlock< SummaryAttributes >( {
|
||||
name,
|
||||
metadata,
|
||||
settings,
|
||||
} );
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './paragraph-rtl-control';
|
||||
export * from './types';
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
import { ToolbarButton } from '@wordpress/components';
|
||||
import { _x, isRTL } from '@wordpress/i18n';
|
||||
import { formatLtr } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ParagraphRTLControlProps } from './types';
|
||||
|
||||
export function ParagraphRTLControl( {
|
||||
direction,
|
||||
onChange,
|
||||
}: ParagraphRTLControlProps ) {
|
||||
function handleClick() {
|
||||
if ( typeof onChange === 'function' ) {
|
||||
onChange( direction === 'ltr' ? undefined : 'ltr' );
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isRTL() && (
|
||||
<ToolbarButton
|
||||
icon={ formatLtr }
|
||||
title={ _x(
|
||||
'Left to right',
|
||||
'editor button',
|
||||
'woocommerce'
|
||||
) }
|
||||
isActive={ direction === 'ltr' }
|
||||
onClick={ handleClick }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SummaryAttributes } from '../types';
|
||||
|
||||
export type ParagraphRTLControlProps = Pick<
|
||||
SummaryAttributes,
|
||||
'direction'
|
||||
> & {
|
||||
onChange( direction?: SummaryAttributes[ 'direction' ] ): void;
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
// This alignment class does not exists in
|
||||
// https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/common.scss
|
||||
.has-text-align-justify {
|
||||
/*rtl:ignore*/
|
||||
text-align: justify;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export type SummaryAttributes = {
|
||||
align: 'left' | 'center' | 'right' | 'justify';
|
||||
direction: 'ltr' | 'rtl';
|
||||
label: string;
|
||||
};
|
|
@ -11,8 +11,8 @@ import { createElement } from '@wordpress/element';
|
|||
*/
|
||||
import { EditProductLinkModal } from '../';
|
||||
|
||||
jest.mock( '@woocommerce/product-editor', () => ( {
|
||||
__experimentalUseProductHelper: jest.fn().mockReturnValue( {
|
||||
jest.mock( '../../../hooks/use-product-helper', () => ( {
|
||||
useProductHelper: jest.fn().mockReturnValue( {
|
||||
updateProductWithStatus: jest.fn(),
|
||||
isUpdatingDraft: jest.fn(),
|
||||
isUpdatingPublished: jest.fn(),
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
EditorSettings,
|
||||
EditorBlockListSettings,
|
||||
} from '@wordpress/block-editor';
|
||||
import { SlotFillProvider } from '@wordpress/components';
|
||||
import { Popover, SlotFillProvider } from '@wordpress/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
|
@ -60,6 +60,8 @@ export function Editor( { product, settings }: EditorProps ) {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Popover.Slot />
|
||||
</SlotFillProvider>
|
||||
</ShortcutProvider>
|
||||
</EntityProvider>
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerCoreBlocks } from '@wordpress/block-library';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { init as initName } from '../details-name-block';
|
||||
import { init as initSummary } from '../details-summary-block';
|
||||
import { init as initSection } from '../section';
|
||||
import { init as initTab } from '../tab';
|
||||
import { init as initPricing } from '../pricing-block';
|
||||
import { init as initCollapsible } from '../collapsible-block';
|
||||
|
||||
export const initBlocks = () => {
|
||||
registerCoreBlocks();
|
||||
initName();
|
||||
initSummary();
|
||||
initSection();
|
||||
initTab();
|
||||
initPricing();
|
||||
initCollapsible();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "woocommerce/product-pricing",
|
||||
"description": "A product price block with currency display.",
|
||||
"title": "Product pricing",
|
||||
"category": "widgets",
|
||||
"keywords": [ "products", "price" ],
|
||||
"textdomain": "default",
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"showPricingSection": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createElement, useContext, Fragment } from '@wordpress/element';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { Link } from '@woocommerce/components';
|
||||
import { useBlockProps } from '@wordpress/block-editor';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { BlockAttributes } from '@wordpress/blocks';
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { formatCurrencyDisplayValue } from '../../utils';
|
||||
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
|
||||
|
||||
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
|
||||
const blockProps = useBlockProps();
|
||||
const { name, label, showPricingSection = false } = attributes;
|
||||
const [ regularPrice, setRegularPrice ] = useEntityProp< string >(
|
||||
'postType',
|
||||
'product',
|
||||
name
|
||||
);
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
const inputProps = useCurrencyInputProps( {
|
||||
value: regularPrice,
|
||||
setValue: setRegularPrice,
|
||||
} );
|
||||
|
||||
const taxSettingsElement = showPricingSection
|
||||
? interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Manage more settings in {{link}}Pricing.{{/link}}',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ `${ getSetting(
|
||||
'adminUrl'
|
||||
) }admin.php?page=wc-settings&tab=tax` }
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_pricing_list_price_help_tax_settings_click'
|
||||
);
|
||||
} }
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} )
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div { ...blockProps }>
|
||||
<BaseControl
|
||||
id={ 'product_pricing_' + name }
|
||||
help={ taxSettingsElement ? taxSettingsElement : '' }
|
||||
>
|
||||
<InputControl
|
||||
name={ name }
|
||||
onChange={ setRegularPrice }
|
||||
label={ label || __( 'Price', 'woocommerce' ) }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( regularPrice ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
{ ...inputProps }
|
||||
/>
|
||||
</BaseControl>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { initBlock } from '../../utils';
|
||||
import metadata from './block.json';
|
||||
import { Edit } from './edit';
|
||||
|
||||
const { name } = metadata;
|
||||
|
||||
export { metadata, name };
|
||||
|
||||
export const settings = {
|
||||
example: {},
|
||||
edit: Edit,
|
||||
};
|
||||
|
||||
export const init = () =>
|
||||
initBlock( {
|
||||
name,
|
||||
metadata: metadata as never,
|
||||
settings,
|
||||
} );
|
|
@ -1,2 +1,3 @@
|
|||
export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper';
|
||||
export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order';
|
||||
export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props';
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CurrencyContext } from '@woocommerce/currency';
|
||||
import { useContext } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useProductHelper } from './use-product-helper';
|
||||
|
||||
export type CurrencyInputProps = {
|
||||
prefix: string;
|
||||
className: string;
|
||||
sanitize: ( value: string | number ) => string;
|
||||
onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
||||
onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
setValue: ( value: string ) => void;
|
||||
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
||||
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
||||
};
|
||||
|
||||
export const useCurrencyInputProps = ( {
|
||||
value,
|
||||
setValue,
|
||||
onFocus,
|
||||
onKeyUp,
|
||||
}: Props ) => {
|
||||
const { sanitizePrice } = useProductHelper();
|
||||
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
|
||||
const currencyInputProps: CurrencyInputProps = {
|
||||
prefix: currencyConfig.symbol,
|
||||
className: 'half-width-field components-currency-control',
|
||||
sanitize: ( val: string | number ) => {
|
||||
return sanitizePrice( String( val ) );
|
||||
},
|
||||
onFocus( event: React.FocusEvent< HTMLInputElement > ) {
|
||||
// In some browsers like safari .select() function inside
|
||||
// the onFocus event doesn't work as expected because it
|
||||
// conflicts with onClick the first time user click the
|
||||
// input. Using setTimeout defers the text selection and
|
||||
// avoid the unexpected behaviour.
|
||||
setTimeout(
|
||||
function deferSelection( element: HTMLInputElement ) {
|
||||
element.select();
|
||||
},
|
||||
0,
|
||||
event.currentTarget
|
||||
);
|
||||
if ( onFocus ) {
|
||||
onFocus( event );
|
||||
}
|
||||
},
|
||||
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
|
||||
const amount = Number.parseFloat( sanitizePrice( value || '0' ) );
|
||||
const step = Number( event.currentTarget.step || '1' );
|
||||
if ( event.code === 'ArrowUp' ) {
|
||||
setValue( String( amount + step ) );
|
||||
}
|
||||
if ( event.code === 'ArrowDown' ) {
|
||||
setValue( String( amount - step ) );
|
||||
}
|
||||
if ( onKeyUp ) {
|
||||
onKeyUp( event );
|
||||
}
|
||||
},
|
||||
};
|
||||
return currencyInputProps;
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
# useValidation
|
||||
|
||||
This custom hook uses the helper functions `const { lockPostSaving, unlockPostSaving } = useDispatch( 'core/editor' );` to lock/unlock the current editing product before saving it.
|
||||
|
||||
## Usage
|
||||
|
||||
Syncronous validation
|
||||
|
||||
```typescript
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { useValidation } from '@woocommerce/product-editor';
|
||||
|
||||
const product = ...;
|
||||
|
||||
const validateTitle = useCallback( (): boolean => {
|
||||
if ( product.title.length < 2 ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [ product.title ] );
|
||||
|
||||
const isTitleValid = useValidation( 'product/title', validateTitle );
|
||||
```
|
||||
|
||||
Asyncronous validation
|
||||
|
||||
```typescript
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import { useValidation } from '@woocommerce/product-editor';
|
||||
|
||||
const product = ...;
|
||||
|
||||
const validateSlug = useCallback( async (): Promise< boolean > => {
|
||||
return fetch( `.../validate-slug?slug=${ product.slug }` )
|
||||
.then( ( response ) => response.json() )
|
||||
.then( ( { isValid } ) => isValid )
|
||||
.catch( () => false );
|
||||
}, [ product.slug ] );
|
||||
|
||||
const isSlugValid = useValidation( 'product/slug', validateSlug );
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
export * from './use-validation';
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useValidation } from '../use-validation';
|
||||
|
||||
jest.mock( '@wordpress/data', () => ( {
|
||||
useDispatch: jest.fn(),
|
||||
} ) );
|
||||
|
||||
describe( 'useValidation', () => {
|
||||
const useDispatchMock = useDispatch as jest.Mock;
|
||||
const lockPostSaving = jest.fn();
|
||||
const unlockPostSaving = jest.fn();
|
||||
|
||||
beforeEach( () => {
|
||||
useDispatchMock.mockReturnValue( {
|
||||
lockPostSaving,
|
||||
unlockPostSaving,
|
||||
} );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
jest.clearAllMocks();
|
||||
} );
|
||||
|
||||
describe( 'sync', () => {
|
||||
it( 'should lock the editor if validate returns false', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useValidation( 'product/name', () => false )
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current ).toBeFalsy();
|
||||
expect( lockPostSaving ).toHaveBeenCalled();
|
||||
expect( unlockPostSaving ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'should unlock the editor if validate returns true', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useValidation( 'product/name', () => true )
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current ).toBeTruthy();
|
||||
expect( lockPostSaving ).not.toHaveBeenCalled();
|
||||
expect( unlockPostSaving ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'async', () => {
|
||||
it( 'should lock the editor if validate resolves false', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useValidation( 'product/name', () => Promise.resolve( false ) )
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current ).toBeFalsy();
|
||||
expect( lockPostSaving ).toHaveBeenCalled();
|
||||
expect( unlockPostSaving ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'should lock the editor if validate rejects', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useValidation( 'product/name', () => Promise.reject() )
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current ).toBeFalsy();
|
||||
expect( lockPostSaving ).toHaveBeenCalled();
|
||||
expect( unlockPostSaving ).not.toHaveBeenCalled();
|
||||
} );
|
||||
|
||||
it( 'should unlock the editor if validate resolves true', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook( () =>
|
||||
useValidation( 'product/name', () => Promise.resolve( true ) )
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect( result.current ).toBeTruthy();
|
||||
expect( lockPostSaving ).not.toHaveBeenCalled();
|
||||
expect( unlockPostSaving ).toHaveBeenCalled();
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch } from '@wordpress/data';
|
||||
import { useEffect, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Signals that product saving is locked.
|
||||
*
|
||||
* @param lockName The namespace used to lock the product saving if validation fails.
|
||||
* @param validate The validator function.
|
||||
*/
|
||||
export function useValidation(
|
||||
lockName: string,
|
||||
validate: () => boolean | Promise< boolean >
|
||||
): boolean | undefined {
|
||||
const [ isValid, setIsValid ] = useState< boolean | undefined >();
|
||||
const { lockPostSaving, unlockPostSaving } = useDispatch( 'core/editor' );
|
||||
|
||||
useEffect( () => {
|
||||
let validationResponse = validate();
|
||||
|
||||
if ( typeof validationResponse === 'boolean' ) {
|
||||
validationResponse = Promise.resolve( validationResponse );
|
||||
}
|
||||
|
||||
validationResponse
|
||||
.then( ( isValidationValid ) => {
|
||||
if ( isValidationValid ) {
|
||||
unlockPostSaving( lockName );
|
||||
} else {
|
||||
lockPostSaving( lockName );
|
||||
}
|
||||
setIsValid( isValidationValid );
|
||||
} )
|
||||
.catch( () => {
|
||||
lockPostSaving( lockName );
|
||||
setIsValid( false );
|
||||
} );
|
||||
}, [ lockName, validate, lockPostSaving, unlockPostSaving ] );
|
||||
|
||||
return isValid;
|
||||
}
|
|
@ -8,3 +8,4 @@
|
|||
@import 'components/section/style.scss';
|
||||
@import 'components/tab/style.scss';
|
||||
@import 'components/tabs/style.scss';
|
||||
@import 'components/details-summary-block/style.scss';
|
||||
|
|
|
@ -51,7 +51,9 @@ export const getProductStockStatus = (
|
|||
}
|
||||
|
||||
if ( product.stock_status ) {
|
||||
return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ];
|
||||
return PRODUCT_STOCK_STATUS_LABELS[
|
||||
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
|
||||
];
|
||||
}
|
||||
|
||||
return PRODUCT_STOCK_STATUS_LABELS.instock;
|
||||
|
@ -77,6 +79,8 @@ export const getProductStockStatusClass = (
|
|||
return PRODUCT_STOCK_STATUS_CLASSES.outofstock;
|
||||
}
|
||||
return product.stock_status
|
||||
? PRODUCT_STOCK_STATUS_CLASSES[ product.stock_status ]
|
||||
? PRODUCT_STOCK_STATUS_CLASSES[
|
||||
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
|
||||
]
|
||||
: '';
|
||||
};
|
||||
|
|
|
@ -1,26 +1,31 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration, registerBlockType } from '@wordpress/blocks';
|
||||
import {
|
||||
Block,
|
||||
BlockConfiguration,
|
||||
registerBlockType,
|
||||
} from '@wordpress/blocks';
|
||||
|
||||
interface BlockRepresentation {
|
||||
name: string;
|
||||
metadata: BlockConfiguration;
|
||||
settings: Partial< BlockConfiguration >;
|
||||
interface BlockRepresentation< T extends Record< string, object > > {
|
||||
name?: string;
|
||||
metadata: BlockConfiguration< T >;
|
||||
settings: Partial< BlockConfiguration< T > >;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to register an individual block.
|
||||
*
|
||||
* @param {Object} block The block to be registered.
|
||||
*
|
||||
* @return {?WPBlockType} The block, if it has been successfully registered;
|
||||
* otherwise `undefined`.
|
||||
* @param block The block to be registered.
|
||||
* @return The block, if it has been successfully registered; otherwise `undefined`.
|
||||
*/
|
||||
export const initBlock = ( block: BlockRepresentation ) => {
|
||||
export function initBlock<
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
T extends Record< string, any > = Record< string, any >
|
||||
>( block: BlockRepresentation< T > ): Block< T > | undefined {
|
||||
if ( ! block ) {
|
||||
return;
|
||||
}
|
||||
const { metadata, settings, name } = block;
|
||||
return registerBlockType( { name, ...metadata }, settings );
|
||||
};
|
||||
return registerBlockType< T >( { name, ...metadata }, settings );
|
||||
}
|
||||
|
|
|
@ -6,4 +6,3 @@ declare global {
|
|||
|
||||
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
|
||||
export {};
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
declare module '@woocommerce/settings' {
|
||||
export declare function getAdminLink( path: string ): string;
|
||||
export declare function getSetting< T >(
|
||||
name: string,
|
||||
fallback?: unknown,
|
||||
filter = ( val: unknown, fb: unknown ) =>
|
||||
typeof val !== 'undefined' ? val : fb
|
||||
): T;
|
||||
}
|
||||
|
||||
declare module '@wordpress/core-data' {
|
||||
function useEntityProp< T = unknown >(
|
||||
kind: string,
|
||||
name: string,
|
||||
prop: string,
|
||||
id?: string
|
||||
): [ T, ( value: T ) => void, T ];
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export const SHOWN_FOR_ACTIONS_OPTION_NAME =
|
||||
'woocommerce_ces_shown_for_actions';
|
||||
export const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
||||
'woocommerce_admin_install_timestamp';
|
|
@ -1,3 +0,0 @@
|
|||
export { default as CustomerEffortScoreTracks } from './customer-effort-score-tracks';
|
||||
export { default as CustomerEffortScoreTracksContainer } from './customer-effort-score-tracks-container';
|
||||
export * from './customer-effort-score-modal-container.tsx';
|
|
@ -9,6 +9,7 @@ import { WooFooterItem } from '@woocommerce/admin-layout';
|
|||
import { Pill } from '@woocommerce/components';
|
||||
import {
|
||||
ALLOW_TRACKING_OPTION_NAME,
|
||||
SHOWN_FOR_ACTIONS_OPTION_NAME,
|
||||
STORE_KEY,
|
||||
} from '@woocommerce/customer-effort-score';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
|
@ -17,7 +18,6 @@ import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import './product-mvp-ces-footer.scss';
|
||||
import { SHOWN_FOR_ACTIONS_OPTION_NAME } from './constants';
|
||||
|
||||
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
||||
'woocommerce_ces_product_mvp_ces_action';
|
||||
|
|
|
@ -9,9 +9,3 @@ export const WELCOME_MODAL_DISMISSED_OPTION_NAME =
|
|||
*/
|
||||
export const WELCOME_FROM_CALYPSO_MODAL_DISMISSED_OPTION_NAME =
|
||||
'woocommerce_welcome_from_calypso_modal_dismissed';
|
||||
|
||||
/**
|
||||
* WooCommerce Admin installation timestamp option name.
|
||||
*/
|
||||
export const WOOCOMMERCE_ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
||||
'woocommerce_admin_install_timestamp';
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
import '@wordpress/notices';
|
||||
import { render } from '@wordpress/element';
|
||||
import { CustomerEffortScoreTracksContainer } from '@woocommerce/customer-effort-score';
|
||||
import {
|
||||
withCurrentUserHydration,
|
||||
withSettingsHydration,
|
||||
|
@ -14,7 +15,6 @@ import {
|
|||
import './stylesheets/_index.scss';
|
||||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
import { PageLayout, EmbedLayout, PrimaryLayout as NoticeArea } from './layout';
|
||||
import { CustomerEffortScoreTracksContainer } from './customer-effort-score-tracks';
|
||||
import { EmbeddedBodyLayout } from './embedded-body-layout';
|
||||
import { WcAdminPaymentsGatewaysBannerSlot } from './payments/payments-settings-banner-slotfill';
|
||||
import { WcAdminConflictErrorSlot } from './settings/conflict-error-slotfill.js';
|
||||
|
|
|
@ -17,7 +17,10 @@ import { Children, cloneElement } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { get, isFunction, identity, memoize } from 'lodash';
|
||||
import { parse } from 'qs';
|
||||
import { triggerExitPageCesSurvey } from '@woocommerce/customer-effort-score';
|
||||
import {
|
||||
CustomerEffortScoreModalContainer,
|
||||
triggerExitPageCesSurvey,
|
||||
} from '@woocommerce/customer-effort-score';
|
||||
import { getHistory, getQuery } from '@woocommerce/navigation';
|
||||
import {
|
||||
PLUGINS_STORE_NAME,
|
||||
|
@ -38,7 +41,6 @@ import { Header } from '../header';
|
|||
import { Footer } from './footer';
|
||||
import Notices from './notices';
|
||||
import TransientNotices from './transient-notices';
|
||||
import { CustomerEffortScoreModalContainer } from '../customer-effort-score-tracks';
|
||||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
import '~/activity-panel';
|
||||
import '~/mobile-banner';
|
||||
|
|
|
@ -8,3 +8,4 @@ export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody';
|
|||
export { CardHeaderTitle } from './CardHeaderTitle';
|
||||
export { CardHeaderDescription } from './CardHeaderDescription';
|
||||
export { CenteredSpinner } from './CenteredSpinner';
|
||||
export { CreateNewCampaignModal } from './CreateNewCampaignModal';
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export { useInstalledPlugins } from './useInstalledPlugins';
|
||||
export { useIntroductionBanner } from './useIntroductionBanner';
|
||||
export { useInstalledPluginsWithoutChannels } from './useInstalledPluginsWithoutChannels';
|
||||
export { useRegisteredChannels } from './useRegisteredChannels';
|
||||
export { useRecommendedChannels } from './useRecommendedChannels';
|
||||
export { useCampaignTypes } from './useCampaignTypes';
|
||||
export { useCampaigns } from './useCampaigns';
|
||||
|
|
|
@ -27,13 +27,10 @@ type UseCampaignsType = {
|
|||
/**
|
||||
* Custom hook to get campaigns.
|
||||
*
|
||||
* @param page Page number. First page is `1`.
|
||||
* @param perPage Page size, i.e. number of records in one page.
|
||||
* @param page Page number. Default is `1`.
|
||||
* @param perPage Page size, i.e. number of records in one page. Default is `5`.
|
||||
*/
|
||||
export const useCampaigns = (
|
||||
page: number,
|
||||
perPage: number
|
||||
): UseCampaignsType => {
|
||||
export const useCampaigns = ( page = 1, perPage = 5 ): UseCampaignsType => {
|
||||
const { data: channels } = useRegisteredChannels();
|
||||
|
||||
return useSelect(
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from '~/marketing/data/constants';
|
||||
import { InstalledPlugin } from '~/marketing/types';
|
||||
|
||||
export type UseInstalledPlugins = {
|
||||
installedPlugins: InstalledPlugin[];
|
||||
activatingPlugins: string[];
|
||||
activateInstalledPlugin: ( slug: string ) => void;
|
||||
loadInstalledPluginsAfterActivation: ( slug: string ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to return plugins and methods for "Installed extensions" card.
|
||||
*/
|
||||
export const useInstalledPlugins = (): UseInstalledPlugins => {
|
||||
const { installedPlugins, activatingPlugins } = useSelect( ( select ) => {
|
||||
const { getInstalledPlugins, getActivatingPlugins } =
|
||||
select( STORE_KEY );
|
||||
|
||||
return {
|
||||
installedPlugins: getInstalledPlugins(),
|
||||
activatingPlugins: getActivatingPlugins(),
|
||||
};
|
||||
}, [] );
|
||||
const { activateInstalledPlugin, loadInstalledPluginsAfterActivation } =
|
||||
useDispatch( STORE_KEY );
|
||||
|
||||
return {
|
||||
installedPlugins,
|
||||
activatingPlugins,
|
||||
activateInstalledPlugin,
|
||||
loadInstalledPluginsAfterActivation,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { chain } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from '~/marketing/data/constants';
|
||||
import { InstalledPlugin } from '~/marketing/types';
|
||||
import { useRecommendedChannels } from './useRecommendedChannels';
|
||||
import { useRegisteredChannels } from './useRegisteredChannels';
|
||||
|
||||
export type UseInstalledPluginsWithoutChannels = {
|
||||
data: InstalledPlugin[];
|
||||
activatingPlugins: string[];
|
||||
activateInstalledPlugin: ( slug: string ) => void;
|
||||
loadInstalledPluginsAfterActivation: ( slug: string ) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to return plugins and methods for "Installed extensions" card.
|
||||
* The list of installed plugins does not include registered and recommended marketing channels.
|
||||
*/
|
||||
export const useInstalledPluginsWithoutChannels =
|
||||
(): UseInstalledPluginsWithoutChannels => {
|
||||
const { installedPlugins, activatingPlugins } = useSelect(
|
||||
( select ) => {
|
||||
const { getInstalledPlugins, getActivatingPlugins } =
|
||||
select( STORE_KEY );
|
||||
|
||||
return {
|
||||
installedPlugins:
|
||||
getInstalledPlugins< InstalledPlugin[] >(),
|
||||
activatingPlugins: getActivatingPlugins(),
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
loading: loadingRegisteredChannels,
|
||||
data: dataRegisteredChannels,
|
||||
} = useRegisteredChannels();
|
||||
const {
|
||||
loading: loadingRecommendedChannels,
|
||||
data: dataRecommendedChannels,
|
||||
} = useRecommendedChannels();
|
||||
|
||||
const { activateInstalledPlugin, loadInstalledPluginsAfterActivation } =
|
||||
useDispatch( STORE_KEY );
|
||||
|
||||
const loading = loadingRegisteredChannels || loadingRecommendedChannels;
|
||||
const installedPluginsWithoutChannels = chain( installedPlugins )
|
||||
.differenceWith(
|
||||
dataRegisteredChannels || [],
|
||||
( a, b ) => a.slug === b.slug
|
||||
)
|
||||
.differenceWith(
|
||||
dataRecommendedChannels || [],
|
||||
( a, b ) => a.slug === b.product
|
||||
)
|
||||
.value();
|
||||
|
||||
return {
|
||||
data: loading ? [] : installedPluginsWithoutChannels,
|
||||
activatingPlugins,
|
||||
activateInstalledPlugin,
|
||||
loadInstalledPluginsAfterActivation,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
type UseIntroductionBanner = {
|
||||
loading: boolean;
|
||||
isIntroductionBannerDismissed: boolean;
|
||||
dismissIntroductionBanner: () => void;
|
||||
};
|
||||
|
||||
const OPTION_NAME_BANNER_DISMISSED =
|
||||
'woocommerce_marketing_overview_multichannel_banner_dismissed';
|
||||
const OPTION_VALUE_YES = 'yes';
|
||||
|
||||
export const useIntroductionBanner = (): UseIntroductionBanner => {
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
|
||||
const dismissIntroductionBanner = () => {
|
||||
updateOptions( {
|
||||
[ OPTION_NAME_BANNER_DISMISSED ]: OPTION_VALUE_YES,
|
||||
} );
|
||||
recordEvent( 'marketing_multichannel_banner_dismissed', {} );
|
||||
};
|
||||
|
||||
const { loading, data } = useSelect( ( select ) => {
|
||||
const { getOption, hasFinishedResolution } =
|
||||
select( OPTIONS_STORE_NAME );
|
||||
|
||||
return {
|
||||
loading: ! hasFinishedResolution( 'getOption', [
|
||||
OPTION_NAME_BANNER_DISMISSED,
|
||||
] ),
|
||||
data: getOption( OPTION_NAME_BANNER_DISMISSED ),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return {
|
||||
loading,
|
||||
isIntroductionBannerDismissed: data === OPTION_VALUE_YES,
|
||||
dismissIntroductionBanner,
|
||||
};
|
||||
};
|
|
@ -7,21 +7,23 @@ import userEvent from '@testing-library/user-event';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useCampaigns } from './useCampaigns';
|
||||
import { useCampaignTypes } from '~/marketing/hooks';
|
||||
import { useCampaignTypes, useCampaigns } from '~/marketing/hooks';
|
||||
import { Campaigns } from './Campaigns';
|
||||
|
||||
jest.mock( './useCampaigns', () => ( {
|
||||
useCampaigns: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '~/marketing/hooks', () => ( {
|
||||
useCampaigns: jest.fn(),
|
||||
useCampaignTypes: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( './CreateNewCampaignModal', () => ( {
|
||||
CreateNewCampaignModal: () => <div>Mocked CreateNewCampaignModal</div>,
|
||||
} ) );
|
||||
jest.mock( '~/marketing/components', () => {
|
||||
const originalModule = jest.requireActual( '~/marketing/components' );
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
CreateNewCampaignModal: () => <div>Mocked CreateNewCampaignModal</div>,
|
||||
};
|
||||
} );
|
||||
|
||||
/**
|
||||
* Create a test campaign data object.
|
||||
|
|
|
@ -24,9 +24,11 @@ import {
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CardHeaderTitle } from '~/marketing/components';
|
||||
import { useCampaigns } from './useCampaigns';
|
||||
import { CreateNewCampaignModal } from './CreateNewCampaignModal';
|
||||
import {
|
||||
CardHeaderTitle,
|
||||
CreateNewCampaignModal,
|
||||
} from '~/marketing/components';
|
||||
import { useCampaigns } from '~/marketing/hooks';
|
||||
import './Campaigns.scss';
|
||||
|
||||
const tableCaption = __( 'Campaigns', 'woocommerce' );
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Fragment, useState } from '@wordpress/element';
|
||||
import {
|
||||
Fragment,
|
||||
useState,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
Card,
|
||||
|
@ -32,72 +38,97 @@ type ChannelsProps = {
|
|||
onInstalledAndActivated?: ( pluginSlug: string ) => void;
|
||||
};
|
||||
|
||||
export const Channels: React.FC< ChannelsProps > = ( {
|
||||
registeredChannels,
|
||||
recommendedChannels,
|
||||
onInstalledAndActivated,
|
||||
} ) => {
|
||||
const hasRegisteredChannels = registeredChannels.length >= 1;
|
||||
|
||||
export type ChannelsRef = {
|
||||
/**
|
||||
* State to collapse / expand the recommended channels.
|
||||
* Initial state is expanded if there are no registered channels in first page load.
|
||||
* Scroll into the "Add channels" section in the card.
|
||||
* The section will be expanded, and the "Add channels" button will be in focus.
|
||||
*/
|
||||
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
|
||||
scrollIntoAddChannels: () => void;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-marketing-channels-card">
|
||||
<CardHeader>
|
||||
<CardHeaderTitle>
|
||||
{ __( 'Channels', 'woocommerce' ) }
|
||||
</CardHeaderTitle>
|
||||
{ ! hasRegisteredChannels && (
|
||||
<CardHeaderDescription>
|
||||
{ __(
|
||||
'Start by adding a channel to your store',
|
||||
'woocommerce'
|
||||
) }
|
||||
</CardHeaderDescription>
|
||||
) }
|
||||
</CardHeader>
|
||||
export const Channels = forwardRef< ChannelsRef, ChannelsProps >(
|
||||
(
|
||||
{ registeredChannels, recommendedChannels, onInstalledAndActivated },
|
||||
ref
|
||||
) => {
|
||||
const hasRegisteredChannels = registeredChannels.length >= 1;
|
||||
|
||||
{ /* Registered channels section. */ }
|
||||
{ registeredChannels.map( ( el, idx ) => {
|
||||
return (
|
||||
/**
|
||||
* State to collapse / expand the recommended channels.
|
||||
* Initial state is expanded if there are no registered channels in first page load.
|
||||
*/
|
||||
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
|
||||
const addChannelsButtonRef = useRef< HTMLButtonElement >( null );
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ( {
|
||||
scrollIntoAddChannels: () => {
|
||||
setExpanded( true );
|
||||
addChannelsButtonRef.current?.focus();
|
||||
addChannelsButtonRef.current?.scrollIntoView( {
|
||||
block: 'center',
|
||||
} );
|
||||
},
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-marketing-channels-card">
|
||||
<CardHeader>
|
||||
<CardHeaderTitle>
|
||||
{ __( 'Channels', 'woocommerce' ) }
|
||||
</CardHeaderTitle>
|
||||
{ ! hasRegisteredChannels && (
|
||||
<CardHeaderDescription>
|
||||
{ __(
|
||||
'Start by adding a channel to your store',
|
||||
'woocommerce'
|
||||
) }
|
||||
</CardHeaderDescription>
|
||||
) }
|
||||
</CardHeader>
|
||||
|
||||
{ /* Registered channels section. */ }
|
||||
{ registeredChannels.map( ( el, idx ) => (
|
||||
<Fragment key={ el.slug }>
|
||||
<RegisteredChannelCardBody registeredChannel={ el } />
|
||||
{ idx !== registeredChannels.length - 1 && (
|
||||
<CardDivider />
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
} ) }
|
||||
) ) }
|
||||
|
||||
{ /* Recommended channels section. */ }
|
||||
{ recommendedChannels.length >= 1 && (
|
||||
<div>
|
||||
{ !! hasRegisteredChannels && (
|
||||
<>
|
||||
<CardDivider />
|
||||
<CardBody>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={ () => setExpanded( ! expanded ) }
|
||||
>
|
||||
{ __( 'Add channels', 'woocommerce' ) }
|
||||
<Icon
|
||||
icon={
|
||||
expanded ? chevronUp : chevronDown
|
||||
{ /* Recommended channels section. */ }
|
||||
{ recommendedChannels.length >= 1 && (
|
||||
<div>
|
||||
{ !! hasRegisteredChannels && (
|
||||
<>
|
||||
<CardDivider />
|
||||
<CardBody>
|
||||
<Button
|
||||
ref={ addChannelsButtonRef }
|
||||
variant="link"
|
||||
onClick={ () =>
|
||||
setExpanded( ! expanded )
|
||||
}
|
||||
size={ 24 }
|
||||
/>
|
||||
</Button>
|
||||
</CardBody>
|
||||
</>
|
||||
) }
|
||||
{ !! expanded &&
|
||||
recommendedChannels.map( ( el, idx ) => {
|
||||
return (
|
||||
>
|
||||
{ __( 'Add channels', 'woocommerce' ) }
|
||||
<Icon
|
||||
icon={
|
||||
expanded
|
||||
? chevronUp
|
||||
: chevronDown
|
||||
}
|
||||
size={ 24 }
|
||||
/>
|
||||
</Button>
|
||||
</CardBody>
|
||||
</>
|
||||
) }
|
||||
{ !! expanded &&
|
||||
recommendedChannels.map( ( el, idx ) => (
|
||||
<Fragment key={ el.plugin }>
|
||||
<SmartPluginCardBody
|
||||
plugin={ el }
|
||||
|
@ -110,10 +141,10 @@ export const Channels: React.FC< ChannelsProps > = ( {
|
|||
<CardDivider />
|
||||
) }
|
||||
</Fragment>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
) }
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
) ) }
|
||||
</div>
|
||||
) }
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export { Channels } from './Channels';
|
||||
export type { ChannelsRef } from './Channels';
|
||||
|
|
|
@ -6,8 +6,7 @@ import { render, screen } from '@testing-library/react';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useInstalledPlugins } from '../../hooks';
|
||||
import { useRecommendedPlugins } from './useRecommendedPlugins';
|
||||
import { useRecommendedPluginsWithoutChannels } from './useRecommendedPluginsWithoutChannels';
|
||||
import { DiscoverTools } from './DiscoverTools';
|
||||
|
||||
jest.mock( '@woocommerce/components', () => {
|
||||
|
@ -20,23 +19,20 @@ jest.mock( '@woocommerce/components', () => {
|
|||
};
|
||||
} );
|
||||
|
||||
jest.mock( './useRecommendedPlugins', () => ( {
|
||||
useRecommendedPlugins: jest.fn(),
|
||||
jest.mock( './useRecommendedPluginsWithoutChannels', () => ( {
|
||||
useRecommendedPluginsWithoutChannels: jest.fn(),
|
||||
} ) );
|
||||
|
||||
jest.mock( '../../hooks', () => ( {
|
||||
useInstalledPlugins: jest.fn(),
|
||||
jest.mock( '~/marketing/hooks', () => ( {
|
||||
useInstalledPluginsWithoutChannels: jest.fn( () => ( {} ) ),
|
||||
} ) );
|
||||
|
||||
describe( 'DiscoverTools component', () => {
|
||||
it( 'should render a Spinner when loading is in progress', () => {
|
||||
( useRecommendedPlugins as jest.Mock ).mockReturnValue( {
|
||||
( useRecommendedPluginsWithoutChannels as jest.Mock ).mockReturnValue( {
|
||||
isInitializing: true,
|
||||
isLoading: true,
|
||||
plugins: [],
|
||||
} );
|
||||
( useInstalledPlugins as jest.Mock ).mockReturnValue( {
|
||||
loadInstalledPluginsAfterActivation: jest.fn(),
|
||||
data: [],
|
||||
} );
|
||||
render( <DiscoverTools /> );
|
||||
|
||||
|
@ -44,13 +40,10 @@ describe( 'DiscoverTools component', () => {
|
|||
} );
|
||||
|
||||
it( 'should render message and link when loading is finish and there are no plugins', () => {
|
||||
( useRecommendedPlugins as jest.Mock ).mockReturnValue( {
|
||||
( useRecommendedPluginsWithoutChannels as jest.Mock ).mockReturnValue( {
|
||||
isInitializing: false,
|
||||
isLoading: false,
|
||||
plugins: [],
|
||||
} );
|
||||
( useInstalledPlugins as jest.Mock ).mockReturnValue( {
|
||||
loadInstalledPluginsAfterActivation: jest.fn(),
|
||||
data: [],
|
||||
} );
|
||||
render( <DiscoverTools /> );
|
||||
|
||||
|
@ -66,10 +59,12 @@ describe( 'DiscoverTools component', () => {
|
|||
|
||||
describe( 'With plugins loaded', () => {
|
||||
it( 'should render `direct_install: true` plugins with "Install plugin" button', () => {
|
||||
( useRecommendedPlugins as jest.Mock ).mockReturnValue( {
|
||||
(
|
||||
useRecommendedPluginsWithoutChannels as jest.Mock
|
||||
).mockReturnValue( {
|
||||
isInitializing: false,
|
||||
isLoading: false,
|
||||
plugins: [
|
||||
data: [
|
||||
{
|
||||
title: 'Google Listings and Ads',
|
||||
description:
|
||||
|
@ -95,9 +90,6 @@ describe( 'DiscoverTools component', () => {
|
|||
},
|
||||
],
|
||||
} );
|
||||
( useInstalledPlugins as jest.Mock ).mockReturnValue( {
|
||||
loadInstalledPluginsAfterActivation: jest.fn(),
|
||||
} );
|
||||
render( <DiscoverTools /> );
|
||||
|
||||
// Assert that we have the "Sales channels" tab, the plugin name, the "Built by WooCommerce" pill, and the "Install plugin" button.
|
||||
|
@ -112,10 +104,12 @@ describe( 'DiscoverTools component', () => {
|
|||
} );
|
||||
|
||||
it( 'should render `direct_install: false` plugins with "View details" button', () => {
|
||||
( useRecommendedPlugins as jest.Mock ).mockReturnValue( {
|
||||
(
|
||||
useRecommendedPluginsWithoutChannels as jest.Mock
|
||||
).mockReturnValue( {
|
||||
isInitializing: false,
|
||||
isLoading: false,
|
||||
plugins: [
|
||||
data: [
|
||||
{
|
||||
title: 'WooCommerce Zapier',
|
||||
description:
|
||||
|
@ -136,9 +130,6 @@ describe( 'DiscoverTools component', () => {
|
|||
},
|
||||
],
|
||||
} );
|
||||
( useInstalledPlugins as jest.Mock ).mockReturnValue( {
|
||||
loadInstalledPluginsAfterActivation: jest.fn(),
|
||||
} );
|
||||
render( <DiscoverTools /> );
|
||||
|
||||
// Assert that we have the CRM tab, plugin name, and "View details" button.
|
||||
|
|
|
@ -14,13 +14,13 @@ import {
|
|||
CardBody,
|
||||
CenteredSpinner,
|
||||
} from '~/marketing/components';
|
||||
import { useRecommendedPlugins } from './useRecommendedPlugins';
|
||||
import { useRecommendedPluginsWithoutChannels } from './useRecommendedPluginsWithoutChannels';
|
||||
import { PluginsTabPanel } from './PluginsTabPanel';
|
||||
import './DiscoverTools.scss';
|
||||
|
||||
export const DiscoverTools = () => {
|
||||
const { isInitializing, isLoading, plugins, installAndActivate } =
|
||||
useRecommendedPlugins();
|
||||
const { isInitializing, isLoading, data, installAndActivate } =
|
||||
useRecommendedPluginsWithoutChannels();
|
||||
|
||||
/**
|
||||
* Renders card body.
|
||||
|
@ -38,7 +38,7 @@ export const DiscoverTools = () => {
|
|||
);
|
||||
}
|
||||
|
||||
if ( plugins.length === 0 ) {
|
||||
if ( data.length === 0 ) {
|
||||
return (
|
||||
<CardBody className="woocommerce-marketing-discover-tools-card-body-empty-content">
|
||||
<Icon icon={ trendingUp } size={ 32 } />
|
||||
|
@ -66,7 +66,7 @@ export const DiscoverTools = () => {
|
|||
|
||||
return (
|
||||
<PluginsTabPanel
|
||||
plugins={ plugins }
|
||||
plugins={ data }
|
||||
isLoading={ isLoading }
|
||||
onInstallAndActivate={ installAndActivate }
|
||||
/>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { flatMapDeep, uniqBy } from 'lodash';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { CardDivider, PluginCardBody } from '~/marketing/components';
|
||||
import { useInstalledPlugins } from '~/marketing/hooks';
|
||||
import { useInstalledPluginsWithoutChannels } from '~/marketing/hooks';
|
||||
import { RecommendedPlugin } from '~/marketing/types';
|
||||
import { getInAppPurchaseUrl } from '~/lib/in-app-purchase';
|
||||
import { createNoticesFromResponse } from '~/lib/notices';
|
||||
|
@ -60,7 +60,8 @@ export const PluginsTabPanel = ( {
|
|||
null
|
||||
);
|
||||
const { installAndActivatePlugins } = useDispatch( PLUGINS_STORE_NAME );
|
||||
const { loadInstalledPluginsAfterActivation } = useInstalledPlugins();
|
||||
const { loadInstalledPluginsAfterActivation } =
|
||||
useInstalledPluginsWithoutChannels();
|
||||
|
||||
/**
|
||||
* Install and activate a plugin.
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from '~/marketing/data/constants';
|
||||
import { RecommendedPlugin } from '~/marketing/types';
|
||||
|
||||
const selector = 'getRecommendedPlugins';
|
||||
const category = 'marketing';
|
||||
|
||||
export const useRecommendedPlugins = () => {
|
||||
const { invalidateResolution, installAndActivateRecommendedPlugin } =
|
||||
useDispatch( STORE_KEY );
|
||||
|
||||
const installAndActivate = ( plugin: string ) => {
|
||||
installAndActivateRecommendedPlugin( plugin, category );
|
||||
invalidateResolution( selector, [ category ] );
|
||||
};
|
||||
|
||||
return useSelect( ( select ) => {
|
||||
const { getRecommendedPlugins, hasFinishedResolution } =
|
||||
select( STORE_KEY );
|
||||
const plugins =
|
||||
getRecommendedPlugins< RecommendedPlugin[] >( category );
|
||||
const isLoading = ! hasFinishedResolution( selector, [ category ] );
|
||||
|
||||
return {
|
||||
isInitializing: ! plugins.length && isLoading,
|
||||
isLoading,
|
||||
plugins,
|
||||
installAndActivate,
|
||||
};
|
||||
}, [] );
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { differenceWith } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from '~/marketing/data/constants';
|
||||
import { useRecommendedChannels } from '~/marketing/hooks';
|
||||
import { RecommendedPlugin } from '~/marketing/types';
|
||||
|
||||
type UseRecommendedPluginsWithoutChannels = {
|
||||
/**
|
||||
* Boolean indicating whether it is initializing.
|
||||
*/
|
||||
isInitializing: boolean;
|
||||
|
||||
/**
|
||||
* Boolean indicating whether it is loading.
|
||||
*
|
||||
* This will be true when data is being refetched
|
||||
* after `invalidateResolution` is called in the `installAndActivate` method.
|
||||
*/
|
||||
isLoading: boolean;
|
||||
|
||||
/**
|
||||
* An array of recommended marketing plugins without marketing channels.
|
||||
*/
|
||||
data: RecommendedPlugin[];
|
||||
|
||||
/**
|
||||
* Install and activate a plugin.
|
||||
*/
|
||||
installAndActivate: ( slug: string ) => void;
|
||||
};
|
||||
|
||||
const selector = 'getRecommendedPlugins';
|
||||
const category = 'marketing';
|
||||
|
||||
/**
|
||||
* A hook to return a list of recommended plugins without marketing channels,
|
||||
* and related methods, to be used with the `DiscoverTools` component.
|
||||
*/
|
||||
export const useRecommendedPluginsWithoutChannels =
|
||||
(): UseRecommendedPluginsWithoutChannels => {
|
||||
const {
|
||||
loading: loadingRecommendedPlugins,
|
||||
data: dataRecommendedPlugins,
|
||||
} = useSelect( ( select ) => {
|
||||
const { getRecommendedPlugins, hasFinishedResolution } =
|
||||
select( STORE_KEY );
|
||||
|
||||
return {
|
||||
loading: ! hasFinishedResolution( selector, [ category ] ),
|
||||
data: getRecommendedPlugins< RecommendedPlugin[] >( category ),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const {
|
||||
loading: loadingRecommendedChannels,
|
||||
data: dataRecommendedChannels,
|
||||
} = useRecommendedChannels();
|
||||
|
||||
const { invalidateResolution, installAndActivateRecommendedPlugin } =
|
||||
useDispatch( STORE_KEY );
|
||||
|
||||
const isInitializing =
|
||||
( loadingRecommendedPlugins && ! dataRecommendedPlugins.length ) ||
|
||||
( loadingRecommendedChannels && ! dataRecommendedChannels );
|
||||
|
||||
const loading = loadingRecommendedPlugins || loadingRecommendedChannels;
|
||||
|
||||
const recommendedPluginsWithoutChannels = differenceWith(
|
||||
dataRecommendedPlugins,
|
||||
dataRecommendedChannels || [],
|
||||
( a, b ) => a.product === b.product
|
||||
);
|
||||
|
||||
const installAndActivate = ( slug: string ) => {
|
||||
installAndActivateRecommendedPlugin( slug, category );
|
||||
invalidateResolution( selector, [ category ] );
|
||||
};
|
||||
|
||||
return {
|
||||
isInitializing,
|
||||
isLoading: loading,
|
||||
data: isInitializing ? [] : recommendedPluginsWithoutChannels,
|
||||
installAndActivate,
|
||||
};
|
||||
};
|
|
@ -16,13 +16,13 @@ import {
|
|||
PluginCardBody,
|
||||
} from '~/marketing/components';
|
||||
import { InstalledPlugin } from '~/marketing/types';
|
||||
import { useInstalledPlugins } from '~/marketing/hooks';
|
||||
import { useInstalledPluginsWithoutChannels } from '~/marketing/hooks';
|
||||
|
||||
export const InstalledExtensions = () => {
|
||||
const { installedPlugins, activatingPlugins, activateInstalledPlugin } =
|
||||
useInstalledPlugins();
|
||||
const { data, activatingPlugins, activateInstalledPlugin } =
|
||||
useInstalledPluginsWithoutChannels();
|
||||
|
||||
if ( installedPlugins.length === 0 ) {
|
||||
if ( data.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ export const InstalledExtensions = () => {
|
|||
|
||||
return (
|
||||
<CollapsibleCard header={ __( 'Installed extensions', 'woocommerce' ) }>
|
||||
{ installedPlugins.map( ( el, idx ) => {
|
||||
{ data.map( ( el, idx ) => {
|
||||
return (
|
||||
<Fragment key={ el.slug }>
|
||||
<PluginCardBody
|
||||
|
@ -90,9 +90,7 @@ export const InstalledExtensions = () => {
|
|||
description={ el.description }
|
||||
button={ getButton( el ) }
|
||||
/>
|
||||
{ idx !== installedPlugins.length - 1 && (
|
||||
<CardDivider />
|
||||
) }
|
||||
{ idx !== data.length - 1 && <CardDivider /> }
|
||||
</Fragment>
|
||||
);
|
||||
} ) }
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
.woocommerce-marketing-introduction-banner {
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.woocommerce-marketing-introduction-banner-content {
|
||||
flex: 1 0;
|
||||
margin: 32px 20px 32px 40px;
|
||||
|
||||
.woocommerce-marketing-introduction-banner-title {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
margin-bottom: $gap-smaller;
|
||||
}
|
||||
|
||||
.woocommerce-marketing-introduction-banner-features {
|
||||
color: $gray-700;
|
||||
|
||||
svg {
|
||||
fill: $studio-woocommerce-purple-50;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketing-introduction-banner-buttons {
|
||||
margin-top: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-marketing-introduction-banner-illustration {
|
||||
flex: 0 0 270px;
|
||||
background: linear-gradient(90deg, rgba(247, 237, 247, 0) 5.31%, rgba(196, 152, 217, 0.12) 77.75%),
|
||||
linear-gradient(90deg, rgba(247, 237, 247, 0) 22%, rgba(196, 152, 217, 0.12) 84.6%);
|
||||
|
||||
.woocommerce-marketing-introduction-banner-image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: center / contain no-repeat;
|
||||
}
|
||||
|
||||
.woocommerce-marketing-introduction-banner-close-button {
|
||||
position: absolute;
|
||||
top: $gap-small;
|
||||
right: $gap;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { Card, Flex, FlexItem, FlexBlock, Button } from '@wordpress/components';
|
||||
import { Icon, trendingUp, megaphone, closeSmall } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CreateNewCampaignModal } from '~/marketing/components';
|
||||
import {
|
||||
useRegisteredChannels,
|
||||
useRecommendedChannels,
|
||||
} from '~/marketing/hooks';
|
||||
import './IntroductionBanner.scss';
|
||||
import wooIconUrl from './woo.svg';
|
||||
import illustrationUrl from './illustration.svg';
|
||||
|
||||
type IntroductionBannerProps = {
|
||||
onDismissClick: () => void;
|
||||
onAddChannelsClick: () => void;
|
||||
};
|
||||
|
||||
export const IntroductionBanner = ( {
|
||||
onDismissClick,
|
||||
onAddChannelsClick,
|
||||
}: IntroductionBannerProps ) => {
|
||||
const [ isModalOpen, setModalOpen ] = useState( false );
|
||||
const { data: dataRegistered } = useRegisteredChannels();
|
||||
const { data: dataRecommended } = useRecommendedChannels();
|
||||
|
||||
const showCreateCampaignButton = !! dataRegistered?.length;
|
||||
|
||||
/**
|
||||
* Boolean to display the "Add channels" button in the introduction banner.
|
||||
*
|
||||
* This depends on the number of registered channels,
|
||||
* because if there are no registered channels,
|
||||
* the Channels card will not have the "Add channels" toggle button,
|
||||
* and it does not make sense to display the "Add channels" button in this introduction banner
|
||||
* that will do nothing upon click.
|
||||
*
|
||||
* If there are registered channels and recommended channels,
|
||||
* the Channels card will display the "Add channels" toggle button,
|
||||
* and clicking on the "Add channels" button in this introduction banner
|
||||
* will scroll to the button in Channels card.
|
||||
*/
|
||||
const showAddChannelsButton =
|
||||
!! dataRegistered?.length && !! dataRecommended?.length;
|
||||
|
||||
return (
|
||||
<Card className="woocommerce-marketing-introduction-banner">
|
||||
<div className="woocommerce-marketing-introduction-banner-content">
|
||||
<div className="woocommerce-marketing-introduction-banner-title">
|
||||
{ __(
|
||||
'Reach new customers and increase sales without leaving WooCommerce',
|
||||
'woocommerce'
|
||||
) }
|
||||
</div>
|
||||
<Flex
|
||||
className="woocommerce-marketing-introduction-banner-features"
|
||||
direction="column"
|
||||
gap={ 1 }
|
||||
expanded={ false }
|
||||
>
|
||||
<FlexItem>
|
||||
<Flex>
|
||||
<Icon icon={ trendingUp } />
|
||||
<FlexBlock>
|
||||
{ __(
|
||||
'Reach customers on other sales channels',
|
||||
'woocommerce'
|
||||
) }
|
||||
</FlexBlock>
|
||||
</Flex>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Flex>
|
||||
<Icon icon={ megaphone } />
|
||||
<FlexBlock>
|
||||
{ __(
|
||||
'Advertise with marketing campaigns',
|
||||
'woocommerce'
|
||||
) }
|
||||
</FlexBlock>
|
||||
</Flex>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Flex>
|
||||
<img
|
||||
src={ wooIconUrl }
|
||||
alt={ __( 'WooCommerce logo', 'woocommerce' ) }
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
<FlexBlock>
|
||||
{ __( 'Built by WooCommerce', 'woocommerce' ) }
|
||||
</FlexBlock>
|
||||
</Flex>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
{ ( showCreateCampaignButton || showAddChannelsButton ) && (
|
||||
<Flex
|
||||
className="woocommerce-marketing-introduction-banner-buttons"
|
||||
justify="flex-start"
|
||||
>
|
||||
{ showCreateCampaignButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ () => {
|
||||
setModalOpen( true );
|
||||
} }
|
||||
>
|
||||
{ __( 'Create a campaign', 'woocommerce' ) }
|
||||
</Button>
|
||||
) }
|
||||
{ showAddChannelsButton && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={ onAddChannelsClick }
|
||||
>
|
||||
{ __( 'Add channels', 'woocommerce' ) }
|
||||
</Button>
|
||||
) }
|
||||
</Flex>
|
||||
) }
|
||||
{ isModalOpen && (
|
||||
<CreateNewCampaignModal
|
||||
onRequestClose={ () => setModalOpen( false ) }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
<div className="woocommerce-marketing-introduction-banner-illustration">
|
||||
<Button
|
||||
isSmall
|
||||
className="woocommerce-marketing-introduction-banner-close-button"
|
||||
onClick={ onDismissClick }
|
||||
>
|
||||
<Icon icon={ closeSmall } />
|
||||
</Button>
|
||||
<div
|
||||
className="woocommerce-marketing-introduction-banner-image-placeholder"
|
||||
style={ {
|
||||
backgroundImage: `url("${ illustrationUrl }")`,
|
||||
} }
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1 @@
|
|||
export { IntroductionBanner } from './IntroductionBanner';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue