Merge branch 'trunk' into bump-required-php-to-7.3

This commit is contained in:
Nestor Soriano 2023-03-22 16:43:50 +01:00
commit 74d0841eaf
No known key found for this signature in database
GPG Key ID: 08110F3518C12CAD
82 changed files with 1686 additions and 1334 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View 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 };
};

View File

@ -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"

View File

@ -173,7 +173,7 @@ jobs:
return await script( { core } )
- name: Find PR comment by github-actions[bot]
uses: peter-evans/find-comment@v2
uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
@ -181,7 +181,7 @@ jobs:
body-includes: Test Results Summary
- name: Create or update PR comment
uses: peter-evans/create-or-update-comment@v2
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}

View File

@ -219,7 +219,7 @@ jobs:
return await script( { core } )
- name: Find PR comment by github-actions[bot]
uses: peter-evans/find-comment@v2
uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
@ -227,7 +227,7 @@ jobs:
body-includes: Test Results Summary
- name: Create or update PR comment
uses: peter-evans/create-or-update-comment@v2
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}

View File

@ -0,0 +1,48 @@
name: Remind reviewers to also review the testing instructions.
on:
pull_request:
types: [review_requested]
permissions: {}
jobs:
add-testing-instructions-review-comment:
runs-on: ubuntu-20.04
permissions:
pull-requests: write
steps:
- name: Get the username of requested reviewers
id: get_reviewer_username
run: |
# Retrieves the username of all reviewers and stores them in a comma-separated list
reviewers=$(echo '${{ toJson(github.event.pull_request.requested_reviewers[*].login) }}' | jq -r 'map("@\(.)") | join(", ")')
echo "REVIEWERS=$reviewers" >> $GITHUB_ENV
- name: Get the name of requested teams
id: get_team_name
run: |
# Retrieves the name of all teams asked for review and stores them in a comma-separated list
teams=$(echo '${{ toJson(github.event.pull_request.requested_teams[*].slug) }}' | jq -r 'map("@woocommerce/\(.)") | join(", ")')
echo "TEAMS=$teams" >> $GITHUB_ENV
- name: Find the comment by github-actions[bot] asking for reviewing the testing instructions
uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: please make sure to review the testing instructions
- name: Create or update PR comment asking for reviewers to review the testing instructions
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Hi ${{ env.REVIEWERS }}, ${{ env.TEAMS }}
Apart from reviewing the code changes, please make sure to review the testing instructions as well.
You can follow this guide to find out what good testing instructions should look like:
https://github.com/woocommerce/woocommerce/wiki/Writing-high-quality-testing-instructions
edit-mode: replace

View File

@ -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

View File

@ -1,5 +1,13 @@
== 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**

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding support for modifying fill name to WooHeaderItem.

View File

@ -11,6 +11,20 @@ import {
export const WC_HEADER_SLOT_NAME = 'woocommerce_header_item';
/**
* Get the slot fill name for the generic header slot or a specific header if provided.
*
* @param name Name of the specific header.
* @return string
*/
const getSlotFillName = ( name?: string ) => {
if ( ! name || ! name.length ) {
return WC_HEADER_SLOT_NAME;
}
return `${ WC_HEADER_SLOT_NAME }/${ name }`;
};
/**
* Create a Fill for extensions to add items to the WooCommerce Admin header.
*
@ -26,17 +40,19 @@ export const WC_HEADER_SLOT_NAME = 'woocommerce_header_item';
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.name - Header name.
* @param {Array} param0.children - Node children.
* @param {Array} param0.order - Node order.
*/
export const WooHeaderItem: React.FC< {
name?: string;
children?: React.ReactNode;
order?: number;
} > & {
Slot: React.FC< Slot.Props >;
} = ( { children, order = 1 } ) => {
Slot: React.FC< Slot.Props & { name?: string } >;
} = ( { children, order = 1, name = '' } ) => {
return (
<Fill name={ WC_HEADER_SLOT_NAME }>
<Fill name={ getSlotFillName( name ) }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren( children, order, fillProps );
} }
@ -44,8 +60,8 @@ export const WooHeaderItem: React.FC< {
);
};
WooHeaderItem.Slot = ( { fillProps } ) => (
<Slot name={ WC_HEADER_SLOT_NAME } fillProps={ fillProps }>
WooHeaderItem.Slot = ( { fillProps, name = '' } ) => (
<Slot name={ getSlotFillName( name ) } fillProps={ fillProps }>
{ sortFillsByOrder }
</Slot>
);

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Move additional components to @woocommerce/customer-effort-score.

View File

@ -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",

View File

@ -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';

View File

@ -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 );

View File

@ -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 );

View File

@ -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';

View File

@ -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';

View File

@ -0,0 +1 @@
export * from './use-customer-effort-score-exit-page-tracker';

View File

@ -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';

View File

@ -0,0 +1,2 @@
export * from './customer-effort-score-exit-page';
export * from './get-store-age-in-weeks';

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Adding header slot fill and more menu to header with slot fill.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Fix issue were template was not re-synced when switching between products.

View File

@ -31,6 +31,7 @@
"@automattic/interpolate-components": "^1.2.0",
"@types/lodash": "^4.14.179",
"@types/wordpress__blocks": "^11.0.7",
"@woocommerce/admin-layout": "workspace:*",
"@woocommerce/components": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/data": "workspace:^4.1.0",
@ -52,6 +53,7 @@
"@wordpress/interface": "wp-6.0",
"@wordpress/keyboard-shortcuts": "wp-6.0",
"@wordpress/media-utils": "wp-6.0",
"@wordpress/plugins": "wp-6.0",
"@wordpress/url": "wp-6.0",
"classnames": "^2.3.1",
"lodash": "^4.17.21",
@ -75,6 +77,7 @@
"@types/wordpress__data": "^6.0.2",
"@types/wordpress__editor": "^13.0.0",
"@types/wordpress__media-utils": "^3.0.0",
"@types/wordpress__plugins": "^3.0.0",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/block-editor": "^9.8.0",

View File

@ -94,7 +94,7 @@ export function BlockEditor( {
synchronizeBlocksWithTemplate( [], _settings?.template ),
{}
);
}, [] );
}, [ product.id ] );
if ( ! blocks ) {
return null;

View File

@ -1,7 +1,8 @@
/**
* External dependencies
*/
import { createElement, StrictMode } from '@wordpress/element';
import { createElement, StrictMode, Fragment } from '@wordpress/element';
import { PluginArea } from '@wordpress/plugins';
import {
EditorSettings,
EditorBlockListSettings,
@ -54,10 +55,14 @@ export function Editor( { product, settings }: EditorProps ) {
/>
}
content={
<BlockEditor
settings={ settings }
product={ product }
/>
<>
<BlockEditor
settings={ settings }
product={ product }
/>
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-block-editor" />
</>
}
/>

View File

@ -11,3 +11,8 @@
.interface-interface-skeleton__sidebar {
width: 280px;
}
.interface-interface-skeleton__header{
// Higher than the sidebar which has a z-index of 90.
z-index: 100;
}

View File

@ -7,12 +7,18 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { navigateTo, getNewPath } from '@woocommerce/navigation';
import { WooHeaderItem } from '@woocommerce/admin-layout';
/**
* Internal dependencies
*/
import { AUTO_DRAFT_NAME, getHeaderTitle } from '../../utils';
/**
* Internal dependencies
*/
import { MoreMenu } from './more-menu';
export type HeaderProps = {
productId: number;
productName: string;
@ -85,6 +91,8 @@ export function Header( { productId, productName }: HeaderProps ) {
? __( 'Add', 'woocommerce' )
: __( 'Save', 'woocommerce' ) }
</Button>
<WooHeaderItem.Slot name="product" />
<MoreMenu />
</div>
</div>
);

View File

@ -1 +1,2 @@
export * from './header';
export * from './woo-more-menu-item';

View File

@ -0,0 +1 @@
export * from './more-menu';

View File

@ -0,0 +1,30 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { MoreMenuDropdown } from '@wordpress/interface';
//import { displayShortcut } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import { WooProductMoreMenuItem } from '../woo-more-menu-item';
export const MoreMenu = () => {
return (
<>
<MoreMenuDropdown>
{ ( { onClose }: { onClose: () => void } ) => (
<>
<WooProductMoreMenuItem.Slot
fillProps={ { onClose } }
/>
</>
) }
</MoreMenuDropdown>
</>
);
};

View File

@ -7,4 +7,25 @@
&__actions {
margin-left: auto;
}
.components-popover__content {
min-width: auto;
width: min-content;
}
.woocommerce-product-header__actions {
display: flex;
> * + * {
margin-left: $gap;
}
}
.components-dropdown-menu__toggle {
&.is-opened {
background-color: #1E1E1E;
color: #fff;
}
}
}

View File

@ -0,0 +1 @@
export * from './woo-more-menu-item';

View File

@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { isEmpty } from 'lodash';
import {
createSlotFill,
Slot as BaseSlot,
Fill as BaseFill,
} from '@wordpress/components';
import { createElement, Fragment } from '@wordpress/element';
type WooProductMoreMenuSlot = React.FC< BaseSlot.Props >;
type WooProductMoreMenuFill = React.FC< BaseFill.Props > & {
Slot?: WooProductMoreMenuSlot;
};
type CreateSlotFillReturn = {
Fill: WooProductMoreMenuFill;
Slot: WooProductMoreMenuSlot;
};
const { Fill, Slot }: CreateSlotFillReturn = createSlotFill(
'WooProductMoreMenuItem'
);
Fill.Slot = ( { fillProps } ) => (
<Slot fillProps={ fillProps }>
{ ( fills ) => {
return isEmpty( fills ) ? null : <>{ fills }</>;
} }
</Slot>
);
export const WooProductMoreMenuItem = Fill as WooProductMoreMenuFill & {
Slot: WooProductMoreMenuSlot;
};

View File

@ -10,6 +10,7 @@ export { DetailsFeatureField as __experimentalDetailsFeatureField } from './deta
export { DetailsCategoriesField as __experimentalDetailsCategoriesField } from './details-categories-field';
export { DetailsSummaryField as __experimentalDetailsSummaryField } from './details-summary-field';
export { DetailsDescriptionField as __experimentalDetailsDescriptionField } from './details-description-field';
export { WooProductMoreMenuItem as __experimentalWooProductMoreMenuItem } from './header';
export {
Editor as __experimentalEditor,
ProductEditorSettings,

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -11,7 +11,9 @@ import { getAdminLink } from '@woocommerce/settings';
import { useFormContext } from '@woocommerce/components';
import { Product } from '@woocommerce/data';
export const ProductMVPFeedbackModalContainer: React.FC = () => {
export const ProductMVPFeedbackModalContainer: React.FC< {
productId?: number;
} > = ( { productId: _productId } ) => {
const { values } = useFormContext< Product >();
const { hideProductMVPFeedbackModal } = useDispatch( STORE_KEY );
const { isProductMVPModalVisible } = useSelect( ( select ) => {
@ -21,8 +23,10 @@ export const ProductMVPFeedbackModalContainer: React.FC = () => {
};
} );
const classicEditorUrl = values.id
? getAdminLink( `post.php?post=${ values.id }&action=edit` )
const productId = _productId ?? values.id;
const classicEditorUrl = productId
? getAdminLink( `post.php?post=${ productId }&action=edit` )
: getAdminLink( 'post-new.php?post_type=product' );
const recordScore = ( checked: string[], comments: string ) => {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -1,5 +1,5 @@
export { useIntroductionBanner } from './useIntroductionBanner';
export { useInstalledPlugins } from './useInstalledPlugins';
export { useInstalledPluginsWithoutChannels } from './useInstalledPluginsWithoutChannels';
export { useRegisteredChannels } from './useRegisteredChannels';
export { useRecommendedChannels } from './useRecommendedChannels';
export { useCampaignTypes } from './useCampaignTypes';

View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -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.

View File

@ -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,
};
}, [] );
};

View File

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

View File

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

View File

@ -0,0 +1,75 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
import { getAdminLink } from '@woocommerce/settings';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { MenuItem } from '@wordpress/components';
import {
ALLOW_TRACKING_OPTION_NAME,
STORE_KEY as CES_STORE_KEY,
} from '@woocommerce/customer-effort-score';
/**
* Internal dependencies
*/
import { ClassicEditorIcon } from '../../images/classic-editor-icon';
import { NEW_PRODUCT_MANAGEMENT } from '~/customer-effort-score-tracks/product-mvp-ces-footer';
export const ClassicEditorMenuItem = ( {
onClose,
productId,
}: {
productId: number;
onClose: () => void;
} ) => {
const { showProductMVPFeedbackModal } = useDispatch( CES_STORE_KEY );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { allowTracking, resolving: isLoading } = useSelect( ( select ) => {
const { getOption, hasFinishedResolution } =
select( OPTIONS_STORE_NAME );
const allowTrackingOption =
getOption( ALLOW_TRACKING_OPTION_NAME ) || 'no';
const resolving = ! hasFinishedResolution( 'getOption', [
ALLOW_TRACKING_OPTION_NAME,
] );
return {
allowTracking: allowTrackingOption === 'yes',
resolving,
};
} );
const classicEditorUrl = productId
? getAdminLink( `post.php?post=${ productId }&action=edit` )
: getAdminLink( 'post-new.php?post_type=product' );
if ( isLoading ) {
return null;
}
return (
<MenuItem
onClick={ () => {
if ( allowTracking ) {
updateOptions( {
[ NEW_PRODUCT_MANAGEMENT ]: 'no',
} );
showProductMVPFeedbackModal();
onClose();
} else {
window.location.href = classicEditorUrl;
onClose();
}
} }
icon={ <ClassicEditorIcon /> }
iconPosition="right"
>
{ __( 'Use the classic editor', 'woocommerce' ) }
</MenuItem>
);
};

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { MenuItem } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
/**
* Internal dependencies
*/
import { FeedbackIcon } from '../../images/feedback-icon';
export const FeedbackMenuItem = ( { onClose }: { onClose: () => void } ) => {
const { showCesModal } = useDispatch( CES_STORE_KEY );
return (
<MenuItem
onClick={ () => {
showCesModal(
{
action: 'new_product',
title: __(
"How's your experience with the product editor?",
'woocommerce'
),
firstQuestion: __(
'The product editing screen is easy to use',
'woocommerce'
),
secondQuestion: __(
"The product editing screen's functionality meets my needs",
'woocommerce'
),
},
{ shouldShowComments: () => true },
{
type: 'snackbar',
icon: <span>🌟</span>,
}
);
onClose();
} }
icon={ <FeedbackIcon /> }
iconPosition="right"
>
{ __( 'Share feedback', 'woocommerce' ) }
</MenuItem>
);
};

View File

@ -0,0 +1,2 @@
export * from './feedback-menu-item';
export * from './classic-editor-menu-item';

View File

@ -0,0 +1,54 @@
/**
* External dependencies
*/
import { __experimentalWooProductMoreMenuItem as WooProductMoreMenuItem } from '@woocommerce/product-editor';
import { registerPlugin } from '@wordpress/plugins';
import { WooHeaderItem } from '@woocommerce/admin-layout';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { ProductMVPFeedbackModalContainer } from '~/customer-effort-score-tracks/product-mvp-feedback-modal-container';
import {
FeedbackMenuItem,
ClassicEditorMenuItem,
} from '../fills/more-menu-items';
const MoreMenuFill = ( { onClose }: { onClose: () => void } ) => {
const [ id ] = useEntityProp( 'postType', 'product', 'id' );
return (
<>
<FeedbackMenuItem onClose={ onClose } />
<ClassicEditorMenuItem productId={ id } onClose={ onClose } />
</>
);
};
const ProductHeaderFill = () => {
const [ id ] = useEntityProp( 'postType', 'product', 'id' );
return <ProductMVPFeedbackModalContainer productId={ id } />;
};
registerPlugin( 'wc-admin-more-menu', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-product-block-editor',
render: () => (
<>
<WooProductMoreMenuItem>
{ ( { onClose }: { onClose: () => void } ) => (
<MoreMenuFill onClose={ onClose } />
) }
</WooProductMoreMenuItem>
<WooHeaderItem name="product">
<ProductHeaderFill />
</WooHeaderItem>
</>
),
} );

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor';
import { Product } from '@woocommerce/data';
import { useDispatch, resolveSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
export function useProductEntityRecord(
productId: string | undefined
): Product | undefined {
const { saveEntityRecord } = useDispatch( 'core' );
const [ product, setProduct ] = useState< Product | undefined >(
undefined
);
useEffect( () => {
const getRecordPromise: Promise< Product > = productId
? resolveSelect( 'core' ).getEntityRecord< Product >(
'postType',
'product',
Number.parseInt( productId, 10 )
)
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Incorrect types.
( saveEntityRecord( 'postType', 'product', {
title: AUTO_DRAFT_NAME,
status: 'auto-draft',
} ) as Promise< Product > );
getRecordPromise
.then( ( autoDraftProduct: Product ) => {
setProduct( autoDraftProduct );
} )
.catch( ( e ) => {
setProduct( undefined );
throw e;
} );
}, [ productId ] );
return product;
}

View File

@ -2,53 +2,39 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { DropdownMenu, MenuItem } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { DropdownMenu } from '@wordpress/components';
import { useFormContext } from '@woocommerce/components';
import { useSelect } from '@wordpress/data';
import { WooHeaderItem } from '@woocommerce/admin-layout';
import { getAdminLink } from '@woocommerce/settings';
import { moreVertical } from '@wordpress/icons';
import { OPTIONS_STORE_NAME, Product } from '@woocommerce/data';
import { useFormContext } from '@woocommerce/components';
import {
ALLOW_TRACKING_OPTION_NAME,
STORE_KEY as CES_STORE_KEY,
} from '@woocommerce/customer-effort-score';
import { ALLOW_TRACKING_OPTION_NAME } from '@woocommerce/customer-effort-score';
/**
* Internal dependencies
*/
import { ClassicEditorIcon } from './images/classic-editor-icon';
import { FeedbackIcon } from './images/feedback-icon';
import { NEW_PRODUCT_MANAGEMENT } from '~/customer-effort-score-tracks/product-mvp-ces-footer';
import {
FeedbackMenuItem,
ClassicEditorMenuItem,
} from './fills/more-menu-items';
import './product-more-menu.scss';
export const ProductMoreMenu = () => {
const { values } = useFormContext< Product >();
const { showCesModal, showProductMVPFeedbackModal } =
useDispatch( CES_STORE_KEY );
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
const { allowTracking, resolving: isLoading } = useSelect( ( select ) => {
const { getOption, hasFinishedResolution } =
select( OPTIONS_STORE_NAME );
const allowTrackingOption =
getOption( ALLOW_TRACKING_OPTION_NAME ) || 'no';
const { resolving: isLoading } = useSelect( ( select ) => {
const { hasFinishedResolution } = select( OPTIONS_STORE_NAME );
const resolving = ! hasFinishedResolution( 'getOption', [
ALLOW_TRACKING_OPTION_NAME,
] );
return {
allowTracking: allowTrackingOption === 'yes',
resolving,
};
} );
const classEditorUrl = values.id
? getAdminLink( `post.php?post=${ values.id }&action=edit` )
: getAdminLink( 'post-new.php?post_type=product' );
if ( isLoading ) {
return null;
}
@ -63,55 +49,11 @@ export const ProductMoreMenu = () => {
>
{ ( { onClose } ) => (
<>
<MenuItem
onClick={ () => {
showCesModal(
{
action: 'new_product',
title: __(
"How's your experience with the product editor?",
'woocommerce'
),
firstQuestion: __(
'The product editing screen is easy to use',
'woocommerce'
),
secondQuestion: __(
"The product editing screen's functionality meets my needs",
'woocommerce'
),
},
{ shouldShowComments: () => true },
{
type: 'snackbar',
icon: <span>🌟</span>,
}
);
onClose();
} }
icon={ <FeedbackIcon /> }
iconPosition="right"
>
{ __( 'Share feedback', 'woocommerce' ) }
</MenuItem>
<MenuItem
onClick={ () => {
if ( allowTracking ) {
updateOptions( {
[ NEW_PRODUCT_MANAGEMENT ]: 'no',
} );
showProductMVPFeedbackModal();
onClose();
} else {
window.location.href = classEditorUrl;
onClose();
}
} }
icon={ <ClassicEditorIcon /> }
iconPosition="right"
>
{ __( 'Use the classic editor', 'woocommerce' ) }
</MenuItem>
<FeedbackMenuItem onClose={ onClose } />
<ClassicEditorMenuItem
productId={ values.id }
onClose={ onClose }
/>
</>
) }
</DropdownMenu>

View File

@ -3,26 +3,28 @@
*/
import {
__experimentalEditor as Editor,
AUTO_DRAFT_NAME,
ProductEditorSettings,
} from '@woocommerce/product-editor';
import { Product } from '@woocommerce/data';
import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { Spinner } from '@wordpress/components';
import { useParams } from 'react-router-dom';
/**
* Internal dependencies
*/
import { useProductEntityRecord } from './hooks/use-product-entity-record';
import './product-page.scss';
import './product-block-page.scss';
import './fills/product-block-editor-fills';
declare const productBlockEditorSettings: ProductEditorSettings;
const ProductEditor: React.FC< { product: Product | undefined } > = ( {
product,
} ) => {
export default function ProductPage() {
const { productId } = useParams();
const product = useProductEntityRecord( productId );
if ( ! product?.id ) {
return <Spinner />;
}
@ -33,57 +35,4 @@ const ProductEditor: React.FC< { product: Product | undefined } > = ( {
settings={ productBlockEditorSettings || {} }
/>
);
};
const EditProductEditor: React.FC< { productId: number } > = ( {
productId,
} ) => {
const { product } = useSelect(
( select: typeof WPSelect ) => {
const { getEntityRecord } = select( 'core' );
return {
product: getEntityRecord(
'postType',
'product',
productId
) as Product,
};
},
[ productId ]
);
return <ProductEditor product={ product } />;
};
const AddProductEditor = () => {
const { saveEntityRecord } = useDispatch( 'core' );
const [ product, setProduct ] = useState< Product | undefined >(
undefined
);
useEffect( () => {
saveEntityRecord( 'postType', 'product', {
title: AUTO_DRAFT_NAME,
status: 'auto-draft',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Incorrect types.
} ).then( ( autoDraftProduct: Product ) => {
setProduct( autoDraftProduct );
} );
}, [] );
return <ProductEditor product={ product } />;
};
export default function ProductPage() {
const { productId } = useParams();
if ( productId ) {
return (
<EditProductEditor productId={ Number.parseInt( productId, 10 ) } />
);
}
return <AddProductEditor />;
}

View File

@ -10,8 +10,11 @@ import { OPTIONS_STORE_NAME, WCDataSelector, WEEK } from '@woocommerce/data';
import { Button, Card, CardHeader } from '@wordpress/components';
import { Text } from '@woocommerce/experimental';
import {
ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
ALLOW_TRACKING_OPTION_NAME,
CustomerFeedbackModal,
CustomerFeedbackSimple,
SHOWN_FOR_ACTIONS_OPTION_NAME,
} from '@woocommerce/customer-effort-score';
import { __ } from '@wordpress/i18n';
@ -27,11 +30,7 @@ type TaskListCompletedHeaderProps = {
customerEffortScore: boolean;
};
const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
'woocommerce_admin_install_timestamp';
const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';
const CUSTOMER_EFFORT_SCORE_ACTION = 'store_setup';
const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';
function getStoreAgeInWeeks( adminInstallTimestamp: number ) {
if ( adminInstallTimestamp === 0 ) {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Refactoring product editor more menu items, and using in block editor slot fills.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Set quantity value when stock tracking is enabled

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Run E2E tests on PR merge to trunk.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Filter out marketing channels in "Installed extensions" and "Discover more marketing tools" cards.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Overwrite clone method to prevent duplicate datq when saving a clone.

View File

@ -0,0 +1,5 @@
Significance: patch
Type: fix
Comment: Revert #36768

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Remove timeouts in e2e tests for variable products and analytics.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Move additional CES-related components to @woocommerce/customer-effort-score.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Replacing multiple components on the block product page with a single hook.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Comment: This PR updates stable tag, no changelog entry is required.

View File

@ -131,6 +131,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
}
}
/**
* This method overwrites the base class's clone method to make it a no-op. In base class WC_Data, we are unsetting the meta_id to clone.
* It seems like this was done to avoid conflicting the metadata when duplicating products. However, doing that does not seems necessary for orders.
* In-fact, when we do that for orders, we lose the capability to clone orders with custom meta data by caching plugins. This is because, when we clone an order object for caching, it will clone the metadata without the ID. Unfortunately, when this cached object with nulled meta ID is retreived, WC_Data will consider it as a new meta and will insert it as a new meta-data causing duplicates.
*
* Eventually, we should move away from overwriting the __clone method in base class itself, since it's easily possible to still duplicate the product without having to hook into the __clone method.
*
* @since 7.6.0
*/
public function __clone() {}
/**
* Get internal type.
*

View File

@ -50,8 +50,8 @@ if ( ! defined( 'ABSPATH' ) ) {
woocommerce_wp_text_input(
array(
'id' => '_stock',
'value' => wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ),
'label' => __( 'Stock quantity', 'woocommerce' ),
'value' => wc_stock_amount( $product_object->get_stock_quantity( 'edit' ) ?? 1 ),
'label' => __( 'Quantity', 'woocommerce' ),
'desc_tip' => true,
'description' => __( 'Stock quantity. If this is a variable product this value will be used to control stock for all variations, unless you define stock at variation level.', 'woocommerce' ),
'type' => 'number',

View File

@ -4,7 +4,7 @@ Tags: online store, ecommerce, shop, shopping cart, sell online, storefront, che
Requires at least: 5.9
Tested up to: 6.1
Requires PHP: 7.2
Stable tag: 7.5.0
Stable tag: 7.5.1
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html

View File

@ -40,11 +40,23 @@ class RemoteInboxNotificationsEngine {
// Hook into WCA updated. This is hooked up here rather than in
// on_admin_init because that runs too late to hook into the action.
WC()->queue()->schedule_single(
time(),
add_action(
'woocommerce_updated',
array( __CLASS__, 'run_on_woocommerce_admin_updated' ),
'woocommerce-remote-inbox-engine'
function() {
$next_hook = WC()->queue()->get_next(
'woocommerce_run_on_woocommerce_admin_updated',
array( __CLASS__, 'run_on_woocommerce_admin_updated' ),
'woocommerce-remote-inbox-engine'
);
if ( null === $next_hook ) {
WC()->queue()->schedule_single(
time(),
'woocommerce_run_on_woocommerce_admin_updated',
array( __CLASS__, 'run_on_woocommerce_admin_updated' ),
'woocommerce-remote-inbox-engine'
);
}
}
);
add_filter( 'woocommerce_get_note_from_db', array( __CLASS__, 'get_note_from_db' ), 10, 1 );

View File

@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Caches;
use Automattic\WooCommerce\Caching\CacheException;
use Automattic\WooCommerce\Caching\ObjectCache;
/**

View File

@ -36,7 +36,13 @@ test.describe( 'Analytics pages', () => {
'//button[@title="Choose which analytics to display and the section name"]'
);
await page.click( 'text=Move up' );
await page.waitForTimeout( 1000 );
// wait for the changes to be saved
await page.waitForResponse(
( response ) =>
response.url().includes( '/users/' ) &&
response.status() === 200
);
}
} );

View File

@ -55,19 +55,28 @@ test.describe.serial( 'Add New Variable Product Page', () => {
if ( i > 0 ) {
await page.click( 'button.add_attribute' );
}
const input = `input[name="attribute_names[${ i }]"]`;
await page.waitForSelector( input, { timeout: 1000 } ); // Wait for up to 1 second
await page.fill( input, `attr #${ i + 1 }` );
await page.fill(
`textarea[name="attribute_values[${ i }]"]`,
'val1 | val2'
);
await page
.locator(
`.woocommerce_attribute_data input[name="attribute_names[${ i }]"]`
)
.fill( `attr #${ i + 1 }` );
await page
.locator(
`.woocommerce_attribute_data textarea[name="attribute_values[${ i }]"]`
)
.fill( 'val1 | val2' );
}
await page.keyboard.press( 'ArrowUp' );
await page.click( 'text=Save attributes' );
await page.waitForTimeout( 1000 ); // Wait for 1 second
// wait for the attributes to be saved
await page.waitForResponse(
( response ) =>
response.url().includes( '/post.php?post=' ) &&
response.status() === 200
);
// Save before going to the Variations tab to prevent variations from all attributes to be automatically created
await page.locator( '#save-post' ).click();
await expect(
@ -206,24 +215,28 @@ test.describe.serial( 'Add New Variable Product Page', () => {
if ( i > 0 ) {
await page.click( 'button.add_attribute' );
}
const input = `input[name="attribute_names[${ i }]"]`;
await page.waitForSelector( input, { timeout: 1000 } ); // Wait for up to 1 seconds
await page.fill( input, `attr #${ i + 1 }` );
await page.fill(
`textarea[name="attribute_values[${ i }]"]`,
'val1 | val2'
);
await page
.locator(
`.woocommerce_attribute_data input[name="attribute_names[${ i }]"]`
)
.fill( `attr #${ i + 1 }` );
await page
.locator(
`.woocommerce_attribute_data textarea[name="attribute_values[${ i }]"]`
)
.fill( 'val1 | val2' );
await page.keyboard.press( 'ArrowUp' );
await page.click( 'text=Save attributes' );
await expect(
page
.locator( '.woocommerce_attribute.closed' )
.filter( { hasText: `attr #${ i + 1 }` } )
).toBeVisible();
}
await page.waitForTimeout( 1000 ); // Wait for 1 second
// wait for the attributes to be saved
await page.waitForResponse(
( response ) =>
response.url().includes( '/post.php?post=' ) &&
response.status() === 200
);
// Save before going to the Variations tab to prevent variations from all attributes to be automatically created
await page.locator( '#save-post' ).click();
await expect(

View File

@ -0,0 +1,51 @@
<?php
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* Class OrderCacheTest.
*/
class OrderCacheTest extends \WC_Unit_Test_Case {
/**
* System under test.
*
* @var OrderCache
*/
private $sut;
/**
* Setup test.
*/
public function setUp(): void {
parent::setUp();
$this->sut = new OrderCache();
}
/**
* Test that the order cache does not cause duplicate data storage.
*/
public function test_meta_is_not_duplicated_when_cached() {
global $wpdb;
if ( ! OrderUtil::orders_cache_usage_is_enabled() ) {
// tip: add HPOS=1 env variable to run this test.
$this->markTestSkipped( 'HPOS based caching is not enabled.' );
}
$order = WC_Helper_Order::create_order();
$order->add_meta_data( 'test', 'test' );
$order->save_meta_data();
$order = wc_get_order( $order->get_id() );
$this->assertTrue( $this->sut->is_cached( $order->get_id() ), 'Order was not cached, but it was expected to be cached. Are you sure that HPOS based caching is enabled.' );
$order2 = wc_get_order( $order->get_id() );
$order2->save_meta_data();
$orders_meta_table = OrdersTableDataStore::get_meta_table_name();
$query = $wpdb->prepare( "SELECT id FROM $orders_meta_table WHERE order_id = %d AND meta_key = %s", $order->get_id(), 'test' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->assertEquals( 1, count( $wpdb->get_col( $query ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Already prepared query.
}
}

File diff suppressed because it is too large Load Diff