Merge branch 'trunk' into e2e/fix-failing-daily-product-variations
This commit is contained in:
commit
ab75a00a83
|
@ -65,3 +65,12 @@
|
||||||
- plugins/woocommerce/src/Admin/**/*
|
- plugins/woocommerce/src/Admin/**/*
|
||||||
- plugins/woocommerce/src/Internal/Admin/**/*
|
- plugins/woocommerce/src/Internal/Admin/**/*
|
||||||
- plugins/woocommerce-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/**/*
|
||||||
|
|
|
@ -31,9 +31,9 @@ jobs:
|
||||||
include:
|
include:
|
||||||
- wp: nightly
|
- wp: nightly
|
||||||
php: '7.4'
|
php: '7.4'
|
||||||
- wp: '5.9'
|
- wp: '6.0'
|
||||||
php: 7.4
|
php: 7.4
|
||||||
- wp: '5.8'
|
- wp: '5.9'
|
||||||
php: 7.4
|
php: 7.4
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
|
|
|
@ -15,12 +15,14 @@ permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.user.login != 'github-actions[bot]' }}
|
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
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
continue-on-error: ${{ matrix.wp == 'nightly' }}
|
continue-on-error: ${{ matrix.wp == 'nightly' }}
|
||||||
|
env:
|
||||||
|
HPOS: ${{ matrix.hpos }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
@ -33,6 +35,9 @@ jobs:
|
||||||
php: 7.4
|
php: 7.4
|
||||||
- wp: '5.9'
|
- wp: '5.9'
|
||||||
php: 7.4
|
php: 7.4
|
||||||
|
- wp: 'latest'
|
||||||
|
php: '7.4'
|
||||||
|
hpos: true
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: mysql:5.6
|
image: mysql:5.6
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
# Duplicate workflow that returns success for this check when there is no relevant file change. See https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
|
|
||||||
|
|
||||||
name: Status Check Bypass for Changelog Only Changes
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '!**'
|
|
||||||
- '**/changelog/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bypass-lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Lint and Test JS"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-7-4-latest:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "PHP 7.4 WP latest"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-8-0-latest:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "PHP 8.0 WP latest"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-api-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Runs API tests."
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-k6:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Runs k6 Performance tests"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-sniff:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Code sniff (PHP 7.4, WP Latest)"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-changelogger-use:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Changelogger use"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Runs E2E tests."
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
bypass-pr-highlight:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "Check pull request changes to highlight"
|
|
||||||
steps:
|
|
||||||
- run: 'echo "No build required"'
|
|
||||||
|
|
|
@ -1,5 +1,70 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 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 =
|
= 7.4.1 2023-03-01 =
|
||||||
|
|
||||||
**WooCommerce**
|
**WooCommerce**
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
"sass": "^1.49.9",
|
"sass": "^1.49.9",
|
||||||
"sass-loader": "^10.2.1",
|
"sass-loader": "^10.2.1",
|
||||||
"syncpack": "^9.8.4",
|
"syncpack": "^9.8.4",
|
||||||
"turbo": "^1.7.0",
|
"turbo": "^1.8.3",
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "^1.1.2",
|
||||||
"webpack": "^5.70.0"
|
"webpack": "^5.70.0"
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update showOtherPaymentMethods() to test latest payment task properly
|
|
@ -31,12 +31,6 @@ export class PaymentsSetup extends BasePage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async showOtherPaymentMethods(): Promise< void > {
|
async showOtherPaymentMethods(): Promise< void > {
|
||||||
const selector = '.woocommerce-task-payments button.toggle-button';
|
|
||||||
await this.page.waitForSelector( selector );
|
|
||||||
const toggleButton = await this.page.$(
|
|
||||||
`${ selector }[aria-expanded=false]`
|
|
||||||
);
|
|
||||||
await toggleButton?.click();
|
|
||||||
await waitForElementByText( 'h2', 'Offline payment methods' );
|
await waitForElementByText( 'h2', 'Offline payment methods' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: dev
|
||||||
|
|
||||||
|
Add CES data store to @woocommerce/customer-effort-score
|
|
@ -4,5 +4,6 @@ export * from './components/customer-feedback-modal';
|
||||||
export * from './components/product-mvp-feedback-modal';
|
export * from './components/product-mvp-feedback-modal';
|
||||||
export * from './components/feedback-modal';
|
export * from './components/feedback-modal';
|
||||||
export * from './hooks/use-customer-effort-score-exit-page-tracker';
|
export * from './hooks/use-customer-effort-score-exit-page-tracker';
|
||||||
|
export * from './store';
|
||||||
export * from './utils/customer-effort-score-exit-page';
|
export * from './utils/customer-effort-score-exit-page';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
|
|
|
@ -11,7 +11,9 @@ import * as actions from './actions';
|
||||||
import * as resolvers from './resolvers';
|
import * as resolvers from './resolvers';
|
||||||
import * as selectors from './selectors';
|
import * as selectors from './selectors';
|
||||||
import reducer from './reducer';
|
import reducer from './reducer';
|
||||||
import { STORE_KEY } from './constants';
|
import { QUEUE_OPTION_NAME, STORE_KEY } from './constants';
|
||||||
|
|
||||||
|
export { QUEUE_OPTION_NAME, STORE_KEY };
|
||||||
|
|
||||||
export default registerStore( STORE_KEY, {
|
export default registerStore( STORE_KEY, {
|
||||||
actions,
|
actions,
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Add tracks for plugin actions and handle plugin error properly
|
|
@ -28,6 +28,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@woocommerce/date": "workspace:*",
|
"@woocommerce/date": "workspace:*",
|
||||||
"@woocommerce/navigation": "workspace:*",
|
"@woocommerce/navigation": "workspace:*",
|
||||||
|
"@woocommerce/tracks": "workspace:*",
|
||||||
"@wordpress/api-fetch": "wp-6.0",
|
"@wordpress/api-fetch": "wp-6.0",
|
||||||
"@wordpress/compose": "wp-6.0",
|
"@wordpress/compose": "wp-6.0",
|
||||||
"@wordpress/core-data": "wp-6.0",
|
"@wordpress/core-data": "wp-6.0",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import { _n, sprintf } from '@wordpress/i18n';
|
import { _n, sprintf } from '@wordpress/i18n';
|
||||||
import { DispatchFromMap } from '@automattic/data-stores';
|
import { DispatchFromMap } from '@automattic/data-stores';
|
||||||
import { controls } from '@wordpress/data';
|
import { controls } from '@wordpress/data';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -49,21 +50,22 @@ const isPluginResponseError = (
|
||||||
typeof error === 'object' && error !== null && plugins[ 0 ] in error;
|
typeof error === 'object' && error !== null && plugins[ 0 ] in error;
|
||||||
|
|
||||||
const formatErrorMessage = (
|
const formatErrorMessage = (
|
||||||
pluginErrors: PluginResponseErrors,
|
actionType: 'install' | 'activate' = 'install',
|
||||||
actionType = 'install'
|
plugins: Partial< PluginNames >[],
|
||||||
|
rawErrorMessage: string
|
||||||
) => {
|
) => {
|
||||||
return sprintf(
|
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.*/
|
/* 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(
|
_n(
|
||||||
'Could not %(actionType)s %(pluginName)s plugin, %(error)s',
|
'Could not %(actionType)s %(pluginName)s plugin, %(error)s',
|
||||||
'Could not %(actionType)s the following plugins: %(pluginName)s with these Errors: %(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'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
actionType,
|
actionType,
|
||||||
pluginName: Object.keys( pluginErrors ).join( ', ' ),
|
pluginName: plugins.join( ', ' ),
|
||||||
error: Object.values( pluginErrors ).join( ', \n' ),
|
error: rawErrorMessage,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -174,35 +176,42 @@ export function setRecommendedPlugins(
|
||||||
}
|
}
|
||||||
|
|
||||||
function* handlePluginAPIError(
|
function* handlePluginAPIError(
|
||||||
actionType: string,
|
actionType: 'install' | 'activate',
|
||||||
plugins: Partial< PluginNames >[],
|
plugins: Partial< PluginNames >[],
|
||||||
error: unknown
|
error: unknown
|
||||||
) {
|
) {
|
||||||
yield setError( 'installPlugins', error );
|
let rawErrorMessage;
|
||||||
|
|
||||||
let pluginResponseError = error;
|
if ( isPluginResponseError( plugins, error ) ) {
|
||||||
if (
|
// Backend error messages are in the form of { plugin-slug: [ error messages ] }.
|
||||||
( error instanceof Error || isRestApiError( error ) ) &&
|
rawErrorMessage = Object.values( error ).join( ', \n' );
|
||||||
plugins[ 0 ]
|
|
||||||
) {
|
|
||||||
pluginResponseError = {
|
|
||||||
[ plugins[ 0 ] ]: [ error.message ],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( isPluginResponseError( plugins, pluginResponseError ) ) {
|
|
||||||
throw new PluginError(
|
|
||||||
formatErrorMessage( pluginResponseError, actionType ),
|
|
||||||
pluginResponseError
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw new PluginError(
|
// Other error such as API connection errors.
|
||||||
`Unexpected Plugin Error: ${ JSON.stringify(
|
rawErrorMessage =
|
||||||
pluginResponseError
|
isRestApiError( error ) || error instanceof Error
|
||||||
) }`,
|
? error.message
|
||||||
pluginResponseError
|
: 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
|
// Action Creator Generators
|
||||||
|
@ -225,6 +234,7 @@ export function* installPlugins( plugins: Partial< PluginNames >[] ) {
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
} catch ( error ) {
|
} catch ( error ) {
|
||||||
|
yield setError( 'installPlugins', error );
|
||||||
yield handlePluginAPIError( 'install', plugins, error );
|
yield handlePluginAPIError( 'install', plugins, error );
|
||||||
} finally {
|
} finally {
|
||||||
yield setIsRequesting( 'installPlugins', false );
|
yield setIsRequesting( 'installPlugins', false );
|
||||||
|
@ -251,6 +261,7 @@ export function* activatePlugins( plugins: Partial< PluginNames >[] ) {
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
} catch ( error ) {
|
} catch ( error ) {
|
||||||
|
yield setError( 'activatePlugins', error );
|
||||||
yield handlePluginAPIError( 'activate', plugins, error );
|
yield handlePluginAPIError( 'activate', plugins, error );
|
||||||
} finally {
|
} finally {
|
||||||
yield setIsRequesting( 'activatePlugins', false );
|
yield setIsRequesting( 'activatePlugins', false );
|
||||||
|
@ -305,7 +316,7 @@ export function* connectToJetpack(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* installJetpackAndConnect(
|
export function* installJetpackAndConnect(
|
||||||
errorAction: ( errorMesage: string ) => void,
|
errorAction: ( errorMessage: string ) => void,
|
||||||
getAdminLink: ( endpoint: string ) => string
|
getAdminLink: ( endpoint: string ) => string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
@ -329,7 +340,7 @@ export function* installJetpackAndConnect(
|
||||||
|
|
||||||
export function* connectToJetpackWithFailureRedirect(
|
export function* connectToJetpackWithFailureRedirect(
|
||||||
failureRedirect: string,
|
failureRedirect: string,
|
||||||
errorAction: ( errorMesage: string ) => void,
|
errorAction: ( errorMessage: string ) => void,
|
||||||
getAdminLink: ( endpoint: string ) => string
|
getAdminLink: ( endpoint: string ) => string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add hook to check unsaved form changes before page navigation
|
|
@ -31,6 +31,7 @@
|
||||||
"@wordpress/compose": "wp-6.0",
|
"@wordpress/compose": "wp-6.0",
|
||||||
"@wordpress/element": "wp-6.0",
|
"@wordpress/element": "wp-6.0",
|
||||||
"@wordpress/hooks": "wp-6.0",
|
"@wordpress/hooks": "wp-6.0",
|
||||||
|
"@wordpress/i18n": "wp-6.0",
|
||||||
"@wordpress/notices": "wp-6.0",
|
"@wordpress/notices": "wp-6.0",
|
||||||
"@wordpress/url": "wp-6.0",
|
"@wordpress/url": "wp-6.0",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useContext, useEffect, useMemo } from '@wordpress/element';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { parseAdminUrl } from '@woocommerce/navigation';
|
import { Location } from 'react-router-dom';
|
||||||
import {
|
import { useEffect, useMemo } from '@wordpress/element';
|
||||||
Location,
|
|
||||||
UNSAFE_NavigationContext as NavigationContext,
|
|
||||||
useLocation,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
export default function usePreventLeavingPage(
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { getHistory } from '../history';
|
||||||
|
import { parseAdminUrl } from '../';
|
||||||
|
|
||||||
|
export const useConfirmUnsavedChanges = (
|
||||||
hasUnsavedChanges: boolean,
|
hasUnsavedChanges: boolean,
|
||||||
shouldConfirm?: ( path: URL, fromUrl: Location ) => boolean,
|
shouldConfirm?: ( path: URL, fromUrl: Location ) => boolean,
|
||||||
/**
|
/**
|
||||||
|
@ -19,24 +20,24 @@ export default function usePreventLeavingPage(
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes
|
||||||
*/
|
*/
|
||||||
message?: string
|
message?: string
|
||||||
) {
|
) => {
|
||||||
const confirmMessage = useMemo(
|
const confirmMessage = useMemo(
|
||||||
() =>
|
() =>
|
||||||
message ??
|
message ??
|
||||||
__( 'Changes you made may not be saved.', 'woocommerce' ),
|
__( 'Changes you made may not be saved.', 'woocommerce' ),
|
||||||
[ message ]
|
[ message ]
|
||||||
);
|
);
|
||||||
const { navigator } = useContext( NavigationContext );
|
const history = getHistory();
|
||||||
const fromUrl = useLocation();
|
|
||||||
|
|
||||||
// This effect prevent react router from navigate and show
|
// This effect prevent react router from navigate and show
|
||||||
// a confirmation message. It's a work around to beforeunload
|
// a confirmation message. It's a work around to beforeunload
|
||||||
// because react router does not triggers that event.
|
// because react router does not triggers that event.
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( hasUnsavedChanges ) {
|
if ( hasUnsavedChanges ) {
|
||||||
const push = navigator.push;
|
const push = history.push;
|
||||||
|
|
||||||
navigator.push = ( ...args: Parameters< typeof push > ) => {
|
history.push = ( ...args: Parameters< typeof push > ) => {
|
||||||
|
const fromUrl = history.location;
|
||||||
const toUrl = parseAdminUrl( args[ 0 ] ) as URL;
|
const toUrl = parseAdminUrl( args[ 0 ] ) as URL;
|
||||||
if (
|
if (
|
||||||
typeof shouldConfirm === 'function' &&
|
typeof shouldConfirm === 'function' &&
|
||||||
|
@ -54,10 +55,10 @@ export default function usePreventLeavingPage(
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
navigator.push = push;
|
history.push = push;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [ navigator, hasUnsavedChanges, confirmMessage ] );
|
}, [ history, hasUnsavedChanges, confirmMessage ] );
|
||||||
|
|
||||||
// This effect listen to the native beforeunload event to show
|
// This effect listen to the native beforeunload event to show
|
||||||
// a confirmation message
|
// a confirmation message
|
||||||
|
@ -79,4 +80,4 @@ export default function usePreventLeavingPage(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [ hasUnsavedChanges, confirmMessage ] );
|
}, [ hasUnsavedChanges, confirmMessage ] );
|
||||||
}
|
};
|
|
@ -18,9 +18,6 @@ import { getAdminLink } from '@woocommerce/settings';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getHistory } from './history';
|
import { getHistory } from './history';
|
||||||
import * as navUtils from './index';
|
|
||||||
|
|
||||||
// For the above, import the module into itself. Functions consumed from this import can be mocked in tests.
|
|
||||||
|
|
||||||
// Expose history so all uses get the same history object.
|
// Expose history so all uses get the same history object.
|
||||||
export { getHistory };
|
export { getHistory };
|
||||||
|
@ -28,6 +25,9 @@ export { getHistory };
|
||||||
// Export all filter utilities
|
// Export all filter utilities
|
||||||
export * from './filters';
|
export * from './filters';
|
||||||
|
|
||||||
|
// Export all hooks
|
||||||
|
export { useConfirmUnsavedChanges } from './hooks/use-confirm-unsaved-changes';
|
||||||
|
|
||||||
const TIME_EXCLUDED_SCREENS_FILTER = 'woocommerce_admin_time_excluded_screens';
|
const TIME_EXCLUDED_SCREENS_FILTER = 'woocommerce_admin_time_excluded_screens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,7 +79,7 @@ export function getNewPath(
|
||||||
* @param {Object} query Query containing the parameters.
|
* @param {Object} query Query containing the parameters.
|
||||||
* @return {Object} Object containing the persisted queries.
|
* @return {Object} Object containing the persisted queries.
|
||||||
*/
|
*/
|
||||||
export const getPersistedQuery = ( query = navUtils.getQuery() ) => {
|
export const getPersistedQuery = ( query = getQuery() ) => {
|
||||||
/**
|
/**
|
||||||
* Filter persisted queries. These query parameters remain in the url when other parameters are updated.
|
* Filter persisted queries. These query parameters remain in the url when other parameters are updated.
|
||||||
*
|
*
|
||||||
|
@ -226,7 +226,7 @@ export function getIdsFromQuery( queryString = '' ) {
|
||||||
* @param {Object} query Query object.
|
* @param {Object} query Query object.
|
||||||
* @return {Array} List of search words.
|
* @return {Array} List of search words.
|
||||||
*/
|
*/
|
||||||
export function getSearchWords( query = navUtils.getQuery() ) {
|
export function getSearchWords( query = getQuery() ) {
|
||||||
if ( typeof query !== 'object' ) {
|
if ( typeof query !== 'object' ) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid parameter passed to getSearchWords, it expects an object or no parameters.'
|
'Invalid parameter passed to getSearchWords, it expects an object or no parameters.'
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add a product header component to the blocks interface
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Improve accessibility around product editor tabs
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add tests around product block editor tabs
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add new pricing block to the product editor package.
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Remove the product block breadcrumbs and sidebar inspector
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Update use of blocks within block editor to always make use of template.
|
|
@ -36,6 +36,7 @@
|
||||||
"@woocommerce/data": "workspace:^4.1.0",
|
"@woocommerce/data": "workspace:^4.1.0",
|
||||||
"@woocommerce/navigation": "workspace:^8.1.0",
|
"@woocommerce/navigation": "workspace:^8.1.0",
|
||||||
"@woocommerce/number": "workspace:*",
|
"@woocommerce/number": "workspace:*",
|
||||||
|
"@woocommerce/settings": "^1.0.0",
|
||||||
"@woocommerce/tracks": "workspace:^1.3.0",
|
"@woocommerce/tracks": "workspace:^1.3.0",
|
||||||
"@wordpress/block-editor": "^9.8.0",
|
"@wordpress/block-editor": "^9.8.0",
|
||||||
"@wordpress/blocks": "^12.3.0",
|
"@wordpress/blocks": "^12.3.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Template } from '@wordpress/blocks';
|
import { synchronizeBlocksWithTemplate, Template } from '@wordpress/blocks';
|
||||||
import {
|
import {
|
||||||
createElement,
|
createElement,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
@ -9,12 +9,9 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { Product } from '@woocommerce/data';
|
import { Product } from '@woocommerce/data';
|
||||||
import { useSelect, select as WPSelect, useDispatch } from '@wordpress/data';
|
import { useSelect, select as WPSelect } from '@wordpress/data';
|
||||||
import { uploadMedia } from '@wordpress/media-utils';
|
import { uploadMedia } from '@wordpress/media-utils';
|
||||||
import {
|
import {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore No types for this exist yet.
|
|
||||||
BlockBreadcrumb,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
BlockContextProvider,
|
BlockContextProvider,
|
||||||
|
@ -24,7 +21,6 @@ import {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
BlockTools,
|
BlockTools,
|
||||||
BlockInspector,
|
|
||||||
EditorSettings,
|
EditorSettings,
|
||||||
EditorBlockListSettings,
|
EditorBlockListSettings,
|
||||||
WritingFlow,
|
WritingFlow,
|
||||||
|
@ -41,7 +37,6 @@ import {
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Sidebar } from '../sidebar';
|
|
||||||
import { Tabs } from '../tabs';
|
import { Tabs } from '../tabs';
|
||||||
|
|
||||||
type BlockEditorProps = {
|
type BlockEditorProps = {
|
||||||
|
@ -59,11 +54,6 @@ export function BlockEditor( {
|
||||||
}: BlockEditorProps ) {
|
}: BlockEditorProps ) {
|
||||||
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
|
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore __experimentalTearDownEditor is not yet included in types package.
|
|
||||||
const { setupEditor, __experimentalTearDownEditor } =
|
|
||||||
useDispatch( 'core/editor' );
|
|
||||||
|
|
||||||
const canUserCreateMedia = useSelect( ( select: typeof WPSelect ) => {
|
const canUserCreateMedia = useSelect( ( select: typeof WPSelect ) => {
|
||||||
const { canUser } = select( 'core' );
|
const { canUser } = select( 'core' );
|
||||||
return canUser( 'create', 'media', '' ) !== false;
|
return canUser( 'create', 'media', '' ) !== false;
|
||||||
|
@ -93,20 +83,19 @@ export function BlockEditor( {
|
||||||
};
|
};
|
||||||
}, [ canUserCreateMedia, _settings ] );
|
}, [ canUserCreateMedia, _settings ] );
|
||||||
|
|
||||||
useLayoutEffect( () => {
|
|
||||||
setupEditor( product, {}, _settings?.template );
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
__experimentalTearDownEditor();
|
|
||||||
};
|
|
||||||
}, [] );
|
|
||||||
|
|
||||||
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
|
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
|
||||||
'postType',
|
'postType',
|
||||||
'product',
|
'product',
|
||||||
{ id: product.id }
|
{ id: product.id }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useLayoutEffect( () => {
|
||||||
|
onChange(
|
||||||
|
synchronizeBlocksWithTemplate( [], _settings?.template ),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}, [] );
|
||||||
|
|
||||||
if ( ! blocks ) {
|
if ( ! blocks ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -121,10 +110,6 @@ export function BlockEditor( {
|
||||||
settings={ settings }
|
settings={ settings }
|
||||||
>
|
>
|
||||||
<Tabs onChange={ setSelectedTab } />
|
<Tabs onChange={ setSelectedTab } />
|
||||||
<BlockBreadcrumb />
|
|
||||||
<Sidebar.InspectorFill>
|
|
||||||
<BlockInspector />
|
|
||||||
</Sidebar.InspectorFill>
|
|
||||||
<div className="editor-styles-wrapper">
|
<div className="editor-styles-wrapper">
|
||||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||||
{ /* @ts-ignore No types for this exist yet. */ }
|
{ /* @ts-ignore No types for this exist yet. */ }
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { Product } from '@woocommerce/data';
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
// eslint-disable-next-line @woocommerce/dependency-group
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
import { EntityProvider } from '@wordpress/core-data';
|
import { EntityProvider } from '@wordpress/core-data';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
// eslint-disable-next-line @woocommerce/dependency-group
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
|
@ -26,14 +25,15 @@ import { FullscreenMode, InterfaceSkeleton } from '@wordpress/interface';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Header } from '../header';
|
import { Header } from '../header';
|
||||||
import { Sidebar } from '../sidebar';
|
|
||||||
import { BlockEditor } from '../block-editor';
|
import { BlockEditor } from '../block-editor';
|
||||||
import { initBlocks } from './init-blocks';
|
import { initBlocks } from './init-blocks';
|
||||||
|
|
||||||
initBlocks();
|
initBlocks();
|
||||||
|
|
||||||
export type ProductEditorSettings = Partial<
|
export type ProductEditorSettings = Partial<
|
||||||
EditorSettings & EditorBlockListSettings
|
EditorSettings & EditorBlockListSettings
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type EditorProps = {
|
type EditorProps = {
|
||||||
product: Product;
|
product: Product;
|
||||||
settings: ProductEditorSettings | undefined;
|
settings: ProductEditorSettings | undefined;
|
||||||
|
@ -47,8 +47,12 @@ export function Editor( { product, settings }: EditorProps ) {
|
||||||
<FullscreenMode isActive={ false } />
|
<FullscreenMode isActive={ false } />
|
||||||
<SlotFillProvider>
|
<SlotFillProvider>
|
||||||
<InterfaceSkeleton
|
<InterfaceSkeleton
|
||||||
header={ <Header title={ product.name } /> }
|
header={
|
||||||
sidebar={ <Sidebar /> }
|
<Header
|
||||||
|
productId={ product.id }
|
||||||
|
productName={ product.name }
|
||||||
|
/>
|
||||||
|
}
|
||||||
content={
|
content={
|
||||||
<BlockEditor
|
<BlockEditor
|
||||||
settings={ settings }
|
settings={ settings }
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { registerCoreBlocks } from '@wordpress/block-library';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { init as initName } from '../details-name-block';
|
import { init as initName } from '../details-name-block';
|
||||||
import { init as initSection } from '../section';
|
import { init as initSection } from '../section';
|
||||||
import { init as initTab } from '../tab';
|
import { init as initTab } from '../tab';
|
||||||
|
import { init as initPricing } from '../pricing-block';
|
||||||
|
|
||||||
export const initBlocks = () => {
|
export const initBlocks = () => {
|
||||||
|
registerCoreBlocks();
|
||||||
initName();
|
initName();
|
||||||
initSection();
|
initSection();
|
||||||
initTab();
|
initTab();
|
||||||
|
initPricing();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,68 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { Product } from '@woocommerce/data';
|
||||||
|
import { Button } from '@wordpress/components';
|
||||||
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { createElement } from '@wordpress/element';
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { navigateTo, getNewPath } from '@woocommerce/navigation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { AUTO_DRAFT_NAME, getHeaderTitle } from '../../utils';
|
||||||
|
|
||||||
export type HeaderProps = {
|
export type HeaderProps = {
|
||||||
title: string;
|
productId: number;
|
||||||
|
productName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Header( { title }: HeaderProps ) {
|
export function Header( { productId, productName }: HeaderProps ) {
|
||||||
|
const { isProductLocked, isSaving, editedProductName } = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { isSavingEntityRecord, getEditedEntityRecord } =
|
||||||
|
select( 'core' );
|
||||||
|
const { isPostSavingLocked } = select( 'core/editor' );
|
||||||
|
|
||||||
|
const product: Product = getEditedEntityRecord(
|
||||||
|
'postType',
|
||||||
|
'product',
|
||||||
|
productId
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isProductLocked: isPostSavingLocked(),
|
||||||
|
isSaving: isSavingEntityRecord(
|
||||||
|
'postType',
|
||||||
|
'product',
|
||||||
|
productId
|
||||||
|
),
|
||||||
|
editedProductName: product?.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[ productId ]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDisabled = isProductLocked || isSaving;
|
||||||
|
const isCreating = productName === AUTO_DRAFT_NAME;
|
||||||
|
|
||||||
|
const { saveEditedEntityRecord } = useDispatch( 'core' );
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
saveEditedEntityRecord< Product >(
|
||||||
|
'postType',
|
||||||
|
'product',
|
||||||
|
productId
|
||||||
|
).then( ( response ) => {
|
||||||
|
if ( isCreating ) {
|
||||||
|
navigateTo( {
|
||||||
|
url: getNewPath( {}, `/product/${ response.id }` ),
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="woocommerce-product-header"
|
className="woocommerce-product-header"
|
||||||
|
@ -16,7 +70,22 @@ export function Header( { title }: HeaderProps ) {
|
||||||
aria-label={ __( 'Product Editor top bar.', 'woocommerce' ) }
|
aria-label={ __( 'Product Editor top bar.', 'woocommerce' ) }
|
||||||
tabIndex={ -1 }
|
tabIndex={ -1 }
|
||||||
>
|
>
|
||||||
<h1 className="woocommerce-product-header__title">{ title }</h1>
|
<h1 className="woocommerce-product-header__title">
|
||||||
|
{ getHeaderTitle( editedProductName, productName ) }
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="woocommerce-product-header__actions">
|
||||||
|
<Button
|
||||||
|
onClick={ handleSave }
|
||||||
|
variant="primary"
|
||||||
|
isBusy={ isSaving }
|
||||||
|
disabled={ isDisabled }
|
||||||
|
>
|
||||||
|
{ isCreating
|
||||||
|
? __( 'Add', 'woocommerce' )
|
||||||
|
: __( 'Save', 'woocommerce' ) }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 $gap;
|
padding: 0 $gap;
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,17 @@
|
||||||
|
/**
|
||||||
|
* 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, settings } );
|
|
@ -1 +0,0 @@
|
||||||
export * from './sidebar';
|
|
|
@ -1,30 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { createElement } from '@wordpress/element';
|
|
||||||
import { createSlotFill, Panel } from '@wordpress/components';
|
|
||||||
|
|
||||||
const { Slot: InspectorSlot, Fill: InspectorFill } = createSlotFill(
|
|
||||||
'ProductBlockEditorSidebarInspector'
|
|
||||||
);
|
|
||||||
|
|
||||||
export function Sidebar() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="woocommerce-product-sidebar"
|
|
||||||
role="region"
|
|
||||||
aria-label={ __(
|
|
||||||
'Product Block Editor advanced settings.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
tabIndex={ -1 }
|
|
||||||
>
|
|
||||||
<Panel header={ __( 'Inspector', 'woocommerce' ) }>
|
|
||||||
<InspectorSlot bubblesVirtually />
|
|
||||||
</Panel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Sidebar.InspectorFill = InspectorFill;
|
|
|
@ -17,7 +17,7 @@ export function Edit( {
|
||||||
}: {
|
}: {
|
||||||
attributes: BlockAttributes;
|
attributes: BlockAttributes;
|
||||||
context?: {
|
context?: {
|
||||||
selectedTab?: string;
|
selectedTab?: string | null;
|
||||||
};
|
};
|
||||||
} ) {
|
} ) {
|
||||||
const blockProps = useBlockProps();
|
const blockProps = useBlockProps();
|
||||||
|
@ -30,13 +30,15 @@ export function Edit( {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
<TabButton
|
<TabButton id={ id } selected={ isSelected }>
|
||||||
id={ id }
|
|
||||||
className={ isSelected ? 'is-selected' : undefined }
|
|
||||||
>
|
|
||||||
{ title }
|
{ title }
|
||||||
</TabButton>
|
</TabButton>
|
||||||
<div className={ classes }>
|
<div
|
||||||
|
id={ `woocommerce-product-tab__${ id }-content` }
|
||||||
|
aria-labelledby={ `woocommerce-product-tab__${ id }` }
|
||||||
|
role="tabpanel"
|
||||||
|
className={ classes }
|
||||||
|
>
|
||||||
<InnerBlocks templateLock="all" />
|
<InnerBlocks templateLock="all" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,14 +15,17 @@ export function TabButton( {
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
selected = false,
|
||||||
}: {
|
}: {
|
||||||
children: string | JSX.Element;
|
children: string | JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
selected?: boolean;
|
||||||
} ) {
|
} ) {
|
||||||
const classes = classnames(
|
const classes = classnames(
|
||||||
'wp-block-woocommerce-product-tab__button',
|
'wp-block-woocommerce-product-tab__button',
|
||||||
className
|
className,
|
||||||
|
{ 'is-selected': selected }
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,6 +37,9 @@ export function TabButton( {
|
||||||
key={ id }
|
key={ id }
|
||||||
className={ classes }
|
className={ classes }
|
||||||
onClick={ () => onClick( id ) }
|
onClick={ () => onClick( id ) }
|
||||||
|
id={ `woocommerce-product-tab__${ id }` }
|
||||||
|
aria-controls={ `woocommerce-product-tab__${ id }-content` }
|
||||||
|
aria-selected={ selected }
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
useState,
|
useState,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { Slot } from '@wordpress/components';
|
import { NavigableMenu, Slot } from '@wordpress/components';
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
// eslint-disable-next-line @woocommerce/dependency-group
|
// eslint-disable-next-line @woocommerce/dependency-group
|
||||||
|
@ -20,7 +20,7 @@ import { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation';
|
||||||
import { TABS_SLOT_NAME } from './constants';
|
import { TABS_SLOT_NAME } from './constants';
|
||||||
|
|
||||||
type TabsProps = {
|
type TabsProps = {
|
||||||
onChange: ( tabId: string | null ) => void;
|
onChange?: ( tabId: string | null ) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TabsFillProps = {
|
export type TabsFillProps = {
|
||||||
|
@ -64,8 +64,20 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectTabOnNavigate(
|
||||||
|
_childIndex: number,
|
||||||
|
child: HTMLButtonElement
|
||||||
|
) {
|
||||||
|
child.click();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-product-tabs">
|
<NavigableMenu
|
||||||
|
role="tablist"
|
||||||
|
onNavigate={ selectTabOnNavigate }
|
||||||
|
className="woocommerce-product-tabs"
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
<Slot
|
<Slot
|
||||||
fillProps={
|
fillProps={
|
||||||
{
|
{
|
||||||
|
@ -79,6 +91,6 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
return <>{ fills }</>;
|
return <>{ fills }</>;
|
||||||
} }
|
} }
|
||||||
</Slot>
|
</Slot>
|
||||||
</div>
|
</NavigableMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { render, fireEvent } from '@testing-library/react';
|
||||||
|
import { getQuery, navigateTo } from '@woocommerce/navigation';
|
||||||
|
import React, { createElement } from 'react';
|
||||||
|
import { SlotFillProvider } from '@wordpress/components';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { Tabs } from '../';
|
||||||
|
import { Edit as Tab } from '../../tab/edit';
|
||||||
|
|
||||||
|
jest.mock( '@wordpress/block-editor', () => ( {
|
||||||
|
...jest.requireActual( '@wordpress/block-editor' ),
|
||||||
|
useBlockProps: jest.fn(),
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
jest.mock( '@woocommerce/navigation', () => ( {
|
||||||
|
...jest.requireActual( '@woocommerce/navigation' ),
|
||||||
|
navigateTo: jest.fn(),
|
||||||
|
getQuery: jest.fn().mockReturnValue( {} ),
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
function MockTabs( { onChange = jest.fn() } ) {
|
||||||
|
const [ selected, setSelected ] = useState< string | null >( null );
|
||||||
|
const mockContext = {
|
||||||
|
selectedTab: selected,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlotFillProvider>
|
||||||
|
<Tabs
|
||||||
|
onChange={ ( tabId ) => {
|
||||||
|
setSelected( tabId );
|
||||||
|
onChange( tabId );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
attributes={ { id: 'test1', title: 'Test button 1' } }
|
||||||
|
context={ mockContext }
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
attributes={ { id: 'test2', title: 'Test button 2' } }
|
||||||
|
context={ mockContext }
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
attributes={ { id: 'test3', title: 'Test button 3' } }
|
||||||
|
context={ mockContext }
|
||||||
|
/>
|
||||||
|
</SlotFillProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe( 'Tabs', () => {
|
||||||
|
beforeEach( () => {
|
||||||
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
|
tab: null,
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should render tab buttons added to the slot', () => {
|
||||||
|
const { queryByText } = render( <MockTabs /> );
|
||||||
|
expect( queryByText( 'Test button 1' ) ).toBeInTheDocument();
|
||||||
|
expect( queryByText( 'Test button 2' ) ).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should set the first tab as active initially', () => {
|
||||||
|
const { queryByText } = render( <MockTabs /> );
|
||||||
|
expect( queryByText( 'Test button 1' ) ).toHaveAttribute(
|
||||||
|
'aria-selected',
|
||||||
|
'true'
|
||||||
|
);
|
||||||
|
expect( queryByText( 'Test button 2' ) ).toHaveAttribute(
|
||||||
|
'aria-selected',
|
||||||
|
'false'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should navigate to a new URL when a tab is clicked', () => {
|
||||||
|
const { getByText } = render( <MockTabs /> );
|
||||||
|
const button = getByText( 'Test button 2' );
|
||||||
|
fireEvent.click( button );
|
||||||
|
|
||||||
|
expect( navigateTo ).toHaveBeenLastCalledWith( {
|
||||||
|
url: 'admin.php?page=wc-admin&tab=test2',
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should select the tab provided in the URL initially', () => {
|
||||||
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
|
tab: 'test2',
|
||||||
|
} );
|
||||||
|
|
||||||
|
const { getByText } = render( <MockTabs /> );
|
||||||
|
|
||||||
|
expect( getByText( 'Test button 2' ) ).toHaveAttribute(
|
||||||
|
'aria-selected',
|
||||||
|
'true'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should select the tab provided on URL change', () => {
|
||||||
|
const { getByText, rerender } = render( <MockTabs /> );
|
||||||
|
|
||||||
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
|
tab: 'test3',
|
||||||
|
} );
|
||||||
|
|
||||||
|
rerender( <MockTabs /> );
|
||||||
|
|
||||||
|
expect( getByText( 'Test button 3' ) ).toHaveAttribute(
|
||||||
|
'aria-selected',
|
||||||
|
'true'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should call the onChange props when changing', async () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
const { rerender } = render( <MockTabs onChange={ mockOnChange } /> );
|
||||||
|
|
||||||
|
expect( mockOnChange ).toHaveBeenCalledWith( 'test1' );
|
||||||
|
|
||||||
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
|
tab: 'test2',
|
||||||
|
} );
|
||||||
|
|
||||||
|
rerender( <MockTabs onChange={ mockOnChange } /> );
|
||||||
|
|
||||||
|
expect( mockOnChange ).toHaveBeenCalledWith( 'test2' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should add a class to the initially selected tab panel', async () => {
|
||||||
|
const { getByRole } = render( <MockTabs /> );
|
||||||
|
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
|
||||||
|
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
|
||||||
|
|
||||||
|
expect( panel1.classList ).toContain( 'is-selected' );
|
||||||
|
expect( panel2.classList ).not.toContain( 'is-selected' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'should add a class to the newly selected tab panel', async () => {
|
||||||
|
const { getByText, getByRole, rerender } = render( <MockTabs /> );
|
||||||
|
const button = getByText( 'Test button 2' );
|
||||||
|
fireEvent.click( button );
|
||||||
|
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
|
||||||
|
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
|
||||||
|
|
||||||
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
|
tab: 'test2',
|
||||||
|
} );
|
||||||
|
|
||||||
|
rerender( <MockTabs /> );
|
||||||
|
|
||||||
|
expect( panel1.classList ).not.toContain( 'is-selected' );
|
||||||
|
expect( panel2.classList ).toContain( 'is-selected' );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -1,2 +1,3 @@
|
||||||
export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper';
|
export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper';
|
||||||
export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order';
|
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;
|
||||||
|
};
|
|
@ -7,3 +7,4 @@ export const ADD_NEW_SHIPPING_CLASS_OPTION_VALUE =
|
||||||
export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized';
|
export const UNCATEGORIZED_CATEGORY_SLUG = 'uncategorized';
|
||||||
export const PRODUCT_VARIATION_TITLE_LIMIT = 32;
|
export const PRODUCT_VARIATION_TITLE_LIMIT = 32;
|
||||||
export const STANDARD_RATE_TAX_CLASS_SLUG = 'standard';
|
export const STANDARD_RATE_TAX_CLASS_SLUG = 'standard';
|
||||||
|
export const AUTO_DRAFT_NAME = 'AUTO-DRAFT';
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { AUTO_DRAFT_NAME } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the header title using the product name.
|
||||||
|
*
|
||||||
|
* @param editedProductName Name value entered for the product.
|
||||||
|
* @param initialProductName Name already persisted to the database.
|
||||||
|
* @return The new title
|
||||||
|
*/
|
||||||
|
export const getHeaderTitle = (
|
||||||
|
editedProductName: string,
|
||||||
|
initialProductName: string
|
||||||
|
): string => {
|
||||||
|
const isProductNameNotEmpty = Boolean( editedProductName );
|
||||||
|
const isProductNameDirty = editedProductName !== initialProductName;
|
||||||
|
const isCreating = initialProductName === AUTO_DRAFT_NAME;
|
||||||
|
|
||||||
|
if ( isProductNameNotEmpty && isProductNameDirty ) {
|
||||||
|
return editedProductName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isCreating ) {
|
||||||
|
return __( 'Add new product', 'woocommerce' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialProductName;
|
||||||
|
};
|
|
@ -51,7 +51,9 @@ export const getProductStockStatus = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( product.stock_status ) {
|
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;
|
return PRODUCT_STOCK_STATUS_LABELS.instock;
|
||||||
|
@ -77,6 +79,8 @@ export const getProductStockStatusClass = (
|
||||||
return PRODUCT_STOCK_STATUS_CLASSES.outofstock;
|
return PRODUCT_STOCK_STATUS_CLASSES.outofstock;
|
||||||
}
|
}
|
||||||
return product.stock_status
|
return product.stock_status
|
||||||
? PRODUCT_STOCK_STATUS_CLASSES[ product.stock_status ]
|
? PRODUCT_STOCK_STATUS_CLASSES[
|
||||||
|
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
|
||||||
|
]
|
||||||
: '';
|
: '';
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
export const AUTO_DRAFT_NAME = 'AUTO-DRAFT';
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { AUTO_DRAFT_NAME } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the product title for use in the header.
|
* Get the product title for use in the header.
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { AUTO_DRAFT_NAME } from './constants';
|
||||||
import { formatCurrencyDisplayValue } from './format-currency-display-value';
|
import { formatCurrencyDisplayValue } from './format-currency-display-value';
|
||||||
import { getCheckboxTracks } from './get-checkbox-tracks';
|
import { getCheckboxTracks } from './get-checkbox-tracks';
|
||||||
import { getCurrencySymbolProps } from './get-currency-symbol-props';
|
import { getCurrencySymbolProps } from './get-currency-symbol-props';
|
||||||
import { getDerivedProductType } from './get-derived-product-type';
|
import { getDerivedProductType } from './get-derived-product-type';
|
||||||
|
import { getHeaderTitle } from './get-header-title';
|
||||||
import { getProductStatus, PRODUCT_STATUS_LABELS } from './get-product-status';
|
import { getProductStatus, PRODUCT_STATUS_LABELS } from './get-product-status';
|
||||||
import {
|
import {
|
||||||
getProductStockStatus,
|
getProductStockStatus,
|
||||||
getProductStockStatusClass,
|
getProductStockStatusClass,
|
||||||
} from './get-product-stock-status';
|
} from './get-product-stock-status';
|
||||||
import { getProductTitle, AUTO_DRAFT_NAME } from './get-product-title';
|
import { getProductTitle } from './get-product-title';
|
||||||
import {
|
import {
|
||||||
getProductVariationTitle,
|
getProductVariationTitle,
|
||||||
getTruncatedProductVariationTitle,
|
getTruncatedProductVariationTitle,
|
||||||
|
@ -27,6 +29,7 @@ export {
|
||||||
getCheckboxTracks,
|
getCheckboxTracks,
|
||||||
getCurrencySymbolProps,
|
getCurrencySymbolProps,
|
||||||
getDerivedProductType,
|
getDerivedProductType,
|
||||||
|
getHeaderTitle,
|
||||||
getProductStatus,
|
getProductStatus,
|
||||||
getProductStockStatus,
|
getProductStockStatus,
|
||||||
getProductStockStatusClass,
|
getProductStockStatusClass,
|
||||||
|
|
|
@ -6,4 +6,3 @@ declare global {
|
||||||
|
|
||||||
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
|
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
|
||||||
export {};
|
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 ];
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import {
|
||||||
import { useSelect, useDispatch } from '@wordpress/data';
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
import { uniqueId, find } from 'lodash';
|
import { uniqueId, find } from 'lodash';
|
||||||
import { Icon, help as helpIcon, external } from '@wordpress/icons';
|
import { Icon, help as helpIcon, external } from '@wordpress/icons';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
import { H, Section } from '@woocommerce/components';
|
import { H, Section } from '@woocommerce/components';
|
||||||
import {
|
import {
|
||||||
ONBOARDING_STORE_NAME,
|
ONBOARDING_STORE_NAME,
|
||||||
|
@ -48,7 +49,6 @@ import { useActiveSetupTasklist } from '~/tasks';
|
||||||
import { LayoutContext } from '~/layout';
|
import { LayoutContext } from '~/layout';
|
||||||
import { getSegmentsFromPath } from '~/utils/url-helpers';
|
import { getSegmentsFromPath } from '~/utils/url-helpers';
|
||||||
import { FeedbackIcon } from '~/products/images/feedback-icon';
|
import { FeedbackIcon } from '~/products/images/feedback-icon';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '~/customer-effort-score-tracks/data/constants';
|
|
||||||
import { ProductFeedbackTour } from '~/guided-tours/add-product-feedback-tour';
|
import { ProductFeedbackTour } from '~/guided-tours/add-product-feedback-tour';
|
||||||
|
|
||||||
const HelpPanel = lazy( () =>
|
const HelpPanel = lazy( () =>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { compose } from '@wordpress/compose';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { omitBy, isUndefined, snakeCase } from 'lodash';
|
import { omitBy, isUndefined, snakeCase } from 'lodash';
|
||||||
import { withSelect, withDispatch } from '@wordpress/data';
|
import { withSelect, withDispatch } from '@wordpress/data';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
import { ReportFilters as Filters } from '@woocommerce/components';
|
import { ReportFilters as Filters } from '@woocommerce/components';
|
||||||
import { SETTINGS_STORE_NAME } from '@woocommerce/data';
|
import { SETTINGS_STORE_NAME } from '@woocommerce/data';
|
||||||
import {
|
import {
|
||||||
|
@ -19,7 +20,6 @@ import { CurrencyContext } from '@woocommerce/currency';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
import { LOCALE } from '~/utils/admin-settings';
|
import { LOCALE } from '~/utils/admin-settings';
|
||||||
|
|
||||||
class ReportFilters extends Component {
|
class ReportFilters extends Component {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { withDispatch, withSelect } from '@wordpress/data';
|
||||||
import { get, noop, partial, uniq } from 'lodash';
|
import { get, noop, partial, uniq } from 'lodash';
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
import { CompareButton, Search, TableCard } from '@woocommerce/components';
|
import { CompareButton, Search, TableCard } from '@woocommerce/components';
|
||||||
import {
|
import {
|
||||||
getIdsFromQuery,
|
getIdsFromQuery,
|
||||||
|
@ -39,7 +40,6 @@ import { recordEvent } from '@woocommerce/tracks';
|
||||||
import DownloadIcon from './download-icon';
|
import DownloadIcon from './download-icon';
|
||||||
import ReportError from '../report-error';
|
import ReportError from '../report-error';
|
||||||
import { extendTableData } from './utils';
|
import { extendTableData } from './utils';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
const TABLE_FILTER = 'woocommerce_admin_report_table';
|
const TABLE_FILTER = 'woocommerce_admin_report_table';
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
import { __, _x } from '@wordpress/i18n';
|
import { __, _x } from '@wordpress/i18n';
|
||||||
import { applyFilters } from '@wordpress/hooks';
|
import { applyFilters } from '@wordpress/hooks';
|
||||||
import { dispatch } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getCategoryLabels } from '../../../lib/async-requests';
|
import { getCategoryLabels } from '../../../lib/async-requests';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
|
|
||||||
const CATEGORY_REPORT_CHARTS_FILTER =
|
const CATEGORY_REPORT_CHARTS_FILTER =
|
||||||
'woocommerce_admin_categories_report_charts';
|
'woocommerce_admin_categories_report_charts';
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
import { __, _x } from '@wordpress/i18n';
|
import { __, _x } from '@wordpress/i18n';
|
||||||
import { applyFilters } from '@wordpress/hooks';
|
import { applyFilters } from '@wordpress/hooks';
|
||||||
import { dispatch } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getCouponLabels } from '../../../lib/async-requests';
|
import { getCouponLabels } from '../../../lib/async-requests';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
|
|
||||||
const COUPON_REPORT_CHARTS_FILTER = 'woocommerce_admin_coupons_report_charts';
|
const COUPON_REPORT_CHARTS_FILTER = 'woocommerce_admin_coupons_report_charts';
|
||||||
const COUPON_REPORT_FILTERS_FILTER = 'woocommerce_admin_coupons_report_filters';
|
const COUPON_REPORT_FILTERS_FILTER = 'woocommerce_admin_coupons_report_filters';
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { __, _x } from '@wordpress/i18n';
|
import { __, _x } from '@wordpress/i18n';
|
||||||
import { applyFilters } from '@wordpress/hooks';
|
import { applyFilters } from '@wordpress/hooks';
|
||||||
import { dispatch } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -12,7 +13,6 @@ import {
|
||||||
getProductLabels,
|
getProductLabels,
|
||||||
getVariationLabels,
|
getVariationLabels,
|
||||||
} from '../../../lib/async-requests';
|
} from '../../../lib/async-requests';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
|
|
||||||
const PRODUCTS_REPORT_CHARTS_FILTER =
|
const PRODUCTS_REPORT_CHARTS_FILTER =
|
||||||
'woocommerce_admin_products_report_charts';
|
'woocommerce_admin_products_report_charts';
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { __, _x } from '@wordpress/i18n';
|
import { __, _x } from '@wordpress/i18n';
|
||||||
import { applyFilters } from '@wordpress/hooks';
|
import { applyFilters } from '@wordpress/hooks';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
import { NAMESPACE } from '@woocommerce/data';
|
import { NAMESPACE } from '@woocommerce/data';
|
||||||
import { dispatch } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
|
|
||||||
|
@ -11,7 +12,6 @@ import { dispatch } from '@wordpress/data';
|
||||||
*/
|
*/
|
||||||
import { getRequestByIdString } from '../../../lib/async-requests';
|
import { getRequestByIdString } from '../../../lib/async-requests';
|
||||||
import { getTaxCode } from './utils';
|
import { getTaxCode } from './utils';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
|
|
||||||
const TAXES_REPORT_CHARTS_FILTER = 'woocommerce_admin_taxes_report_charts';
|
const TAXES_REPORT_CHARTS_FILTER = 'woocommerce_admin_taxes_report_charts';
|
||||||
const TAXES_REPORT_FILTERS_FILTER = 'woocommerce_admin_taxes_report_filters';
|
const TAXES_REPORT_FILTERS_FILTER = 'woocommerce_admin_taxes_report_filters';
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { __, _x } from '@wordpress/i18n';
|
import { __, _x } from '@wordpress/i18n';
|
||||||
import { applyFilters } from '@wordpress/hooks';
|
import { applyFilters } from '@wordpress/hooks';
|
||||||
import { dispatch } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
|
import { STORE_KEY as CES_STORE_KEY } from '@woocommerce/customer-effort-score';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -13,7 +14,6 @@ import {
|
||||||
getProductLabels,
|
getProductLabels,
|
||||||
getVariationLabels,
|
getVariationLabels,
|
||||||
} from '../../../lib/async-requests';
|
} from '../../../lib/async-requests';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '../../../customer-effort-score-tracks/data/constants';
|
|
||||||
|
|
||||||
const VARIATIONS_REPORT_CHARTS_FILTER =
|
const VARIATIONS_REPORT_CHARTS_FILTER =
|
||||||
'woocommerce_admin_variations_report_charts';
|
'woocommerce_admin_variations_report_charts';
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { CustomerFeedbackModal } from '@woocommerce/customer-effort-score';
|
import {
|
||||||
|
CustomerFeedbackModal,
|
||||||
|
STORE_KEY,
|
||||||
|
} from '@woocommerce/customer-effort-score';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
|
||||||
|
@ -12,7 +15,6 @@ import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
*/
|
*/
|
||||||
import { getStoreAgeInWeeks } from './utils';
|
import { getStoreAgeInWeeks } from './utils';
|
||||||
import { ADMIN_INSTALL_TIMESTAMP_OPTION_NAME } from './constants';
|
import { ADMIN_INSTALL_TIMESTAMP_OPTION_NAME } from './constants';
|
||||||
import { STORE_KEY } from './data/constants';
|
|
||||||
|
|
||||||
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
||||||
'woocommerce_ces_product_mvp_ces_action';
|
'woocommerce_ces_product_mvp_ces_action';
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import { withDispatch, withSelect } from '@wordpress/data';
|
import { withDispatch, withSelect } from '@wordpress/data';
|
||||||
|
import {
|
||||||
|
QUEUE_OPTION_NAME,
|
||||||
|
STORE_KEY,
|
||||||
|
} from '@woocommerce/customer-effort-score';
|
||||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
@ -11,8 +15,6 @@ import PropTypes from 'prop-types';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import CustomerEffortScoreTracks from './customer-effort-score-tracks';
|
import CustomerEffortScoreTracks from './customer-effort-score-tracks';
|
||||||
import { STORE_KEY, QUEUE_OPTION_NAME } from './data/constants';
|
|
||||||
import './data';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps the queue of CES tracks surveys to CustomerEffortScoreTracks
|
* Maps the queue of CES tracks surveys to CustomerEffortScoreTracks
|
||||||
|
|
|
@ -7,7 +7,10 @@ import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { closeSmall } from '@wordpress/icons';
|
import { closeSmall } from '@wordpress/icons';
|
||||||
import { WooFooterItem } from '@woocommerce/admin-layout';
|
import { WooFooterItem } from '@woocommerce/admin-layout';
|
||||||
import { Pill } from '@woocommerce/components';
|
import { Pill } from '@woocommerce/components';
|
||||||
import { ALLOW_TRACKING_OPTION_NAME } from '@woocommerce/customer-effort-score';
|
import {
|
||||||
|
ALLOW_TRACKING_OPTION_NAME,
|
||||||
|
STORE_KEY,
|
||||||
|
} from '@woocommerce/customer-effort-score';
|
||||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,7 +18,6 @@ import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
*/
|
*/
|
||||||
import './product-mvp-ces-footer.scss';
|
import './product-mvp-ces-footer.scss';
|
||||||
import { SHOWN_FOR_ACTIONS_OPTION_NAME } from './constants';
|
import { SHOWN_FOR_ACTIONS_OPTION_NAME } from './constants';
|
||||||
import { STORE_KEY } from './data/constants';
|
|
||||||
|
|
||||||
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
||||||
'woocommerce_ces_product_mvp_ces_action';
|
'woocommerce_ces_product_mvp_ces_action';
|
||||||
|
|
|
@ -2,17 +2,15 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useDispatch, useSelect } from '@wordpress/data';
|
import { useDispatch, useSelect } from '@wordpress/data';
|
||||||
import { ProductMVPFeedbackModal } from '@woocommerce/customer-effort-score';
|
import {
|
||||||
|
ProductMVPFeedbackModal,
|
||||||
|
STORE_KEY,
|
||||||
|
} from '@woocommerce/customer-effort-score';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { getAdminLink } from '@woocommerce/settings';
|
import { getAdminLink } from '@woocommerce/settings';
|
||||||
import { useFormContext } from '@woocommerce/components';
|
import { useFormContext } from '@woocommerce/components';
|
||||||
import { Product } from '@woocommerce/data';
|
import { Product } from '@woocommerce/data';
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { STORE_KEY } from './data/constants';
|
|
||||||
|
|
||||||
export const ProductMVPFeedbackModalContainer: React.FC = () => {
|
export const ProductMVPFeedbackModalContainer: React.FC = () => {
|
||||||
const { values } = useFormContext< Product >();
|
const { values } = useFormContext< Product >();
|
||||||
const { hideProductMVPFeedbackModal } = useDispatch( STORE_KEY );
|
const { hideProductMVPFeedbackModal } = useDispatch( STORE_KEY );
|
||||||
|
|
|
@ -89,8 +89,7 @@ export const useJetpackPluginState = () => {
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
jetpackConnectionData &&
|
jetpackConnectionData &&
|
||||||
jetpackConnectionData?.currentUser?.username !==
|
! jetpackConnectionData?.currentUser?.isMaster
|
||||||
jetpackConnectionData?.connectionOwner
|
|
||||||
) {
|
) {
|
||||||
setPluginState(
|
setPluginState(
|
||||||
JetpackPluginStates.NOT_OWNER_OF_CONNECTION
|
JetpackPluginStates.NOT_OWNER_OF_CONNECTION
|
||||||
|
|
|
@ -64,7 +64,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( {
|
||||||
{ ! collapsed && (
|
{ ! collapsed && (
|
||||||
<>
|
<>
|
||||||
{ children }
|
{ children }
|
||||||
{ footer && <CardFooter>{ footer }</CardFooter> }
|
{ !! footer && <CardFooter>{ footer }</CardFooter> }
|
||||||
</>
|
</>
|
||||||
) }
|
) }
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -105,7 +105,7 @@ export const CreateNewCampaignModal = ( props: CreateCampaignModalProps ) => {
|
||||||
<FlexItem>
|
<FlexItem>
|
||||||
{ __( 'Create', 'woocommerce' ) }
|
{ __( 'Create', 'woocommerce' ) }
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
{ isExternalURL( el.createUrl ) && (
|
{ !! isExternalURL( el.createUrl ) && (
|
||||||
<FlexItem>
|
<FlexItem>
|
||||||
<Icon
|
<Icon
|
||||||
icon={ external }
|
icon={ external }
|
|
@ -8,3 +8,4 @@ export { PluginCardBody, SmartPluginCardBody } from './PluginCardBody';
|
||||||
export { CardHeaderTitle } from './CardHeaderTitle';
|
export { CardHeaderTitle } from './CardHeaderTitle';
|
||||||
export { CardHeaderDescription } from './CardHeaderDescription';
|
export { CardHeaderDescription } from './CardHeaderDescription';
|
||||||
export { CenteredSpinner } from './CenteredSpinner';
|
export { CenteredSpinner } from './CenteredSpinner';
|
||||||
|
export { CreateNewCampaignModal } from './CreateNewCampaignModal';
|
||||||
|
|
|
@ -79,7 +79,7 @@ const KnowledgeBase = ( {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{ post.image && (
|
{ !! post.image && (
|
||||||
<div className="woocommerce-marketing-knowledgebase-card__post-img">
|
<div className="woocommerce-marketing-knowledgebase-card__post-img">
|
||||||
<img src={ post.image } alt="" />
|
<img src={ post.image } alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,7 +89,7 @@ const KnowledgeBase = ( {
|
||||||
<p className="woocommerce-marketing-knowledgebase-card__post-meta">
|
<p className="woocommerce-marketing-knowledgebase-card__post-meta">
|
||||||
{ __( 'By', 'woocommerce' ) + ' ' }
|
{ __( 'By', 'woocommerce' ) + ' ' }
|
||||||
{ post.author_name }
|
{ post.author_name }
|
||||||
{ post.author_avatar && (
|
{ !! post.author_avatar && (
|
||||||
<img
|
<img
|
||||||
src={ post.author_avatar.replace(
|
src={ post.author_avatar.replace(
|
||||||
's=96',
|
's=96',
|
||||||
|
|
|
@ -22,7 +22,7 @@ const CouponsOverview = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketing-coupons">
|
<div className="woocommerce-marketing-coupons">
|
||||||
{ shouldShowExtensions && (
|
{ !! shouldShowExtensions && (
|
||||||
<RecommendedExtensions
|
<RecommendedExtensions
|
||||||
title={ __(
|
title={ __(
|
||||||
'Recommended coupon extensions',
|
'Recommended coupon extensions',
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
export { useIntroductionBanner } from './useIntroductionBanner';
|
||||||
export { useInstalledPlugins } from './useInstalledPlugins';
|
export { useInstalledPlugins } from './useInstalledPlugins';
|
||||||
export { useRegisteredChannels } from './useRegisteredChannels';
|
export { useRegisteredChannels } from './useRegisteredChannels';
|
||||||
export { useRecommendedChannels } from './useRecommendedChannels';
|
export { useRecommendedChannels } from './useRecommendedChannels';
|
||||||
export { useCampaignTypes } from './useCampaignTypes';
|
export { useCampaignTypes } from './useCampaignTypes';
|
||||||
|
export { useCampaigns } from './useCampaigns';
|
||||||
|
|
|
@ -27,13 +27,10 @@ type UseCampaignsType = {
|
||||||
/**
|
/**
|
||||||
* Custom hook to get campaigns.
|
* Custom hook to get campaigns.
|
||||||
*
|
*
|
||||||
* @param page Page number. First page is `1`.
|
* @param page Page number. Default is `1`.
|
||||||
* @param perPage Page size, i.e. number of records in one page.
|
* @param perPage Page size, i.e. number of records in one page. Default is `5`.
|
||||||
*/
|
*/
|
||||||
export const useCampaigns = (
|
export const useCampaigns = ( page = 1, perPage = 5 ): UseCampaignsType => {
|
||||||
page: number,
|
|
||||||
perPage: number
|
|
||||||
): UseCampaignsType => {
|
|
||||||
const { data: channels } = useRegisteredChannels();
|
const { data: channels } = useRegisteredChannels();
|
||||||
|
|
||||||
return useSelect(
|
return useSelect(
|
|
@ -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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { useCampaigns } from './useCampaigns';
|
import { useCampaignTypes, useCampaigns } from '~/marketing/hooks';
|
||||||
import { useCampaignTypes } from '~/marketing/hooks';
|
|
||||||
import { Campaigns } from './Campaigns';
|
import { Campaigns } from './Campaigns';
|
||||||
|
|
||||||
jest.mock( './useCampaigns', () => ( {
|
|
||||||
useCampaigns: jest.fn(),
|
|
||||||
} ) );
|
|
||||||
|
|
||||||
jest.mock( '~/marketing/hooks', () => ( {
|
jest.mock( '~/marketing/hooks', () => ( {
|
||||||
|
useCampaigns: jest.fn(),
|
||||||
useCampaignTypes: jest.fn(),
|
useCampaignTypes: jest.fn(),
|
||||||
} ) );
|
} ) );
|
||||||
|
|
||||||
jest.mock( './CreateNewCampaignModal', () => ( {
|
jest.mock( '~/marketing/components', () => {
|
||||||
|
const originalModule = jest.requireActual( '~/marketing/components' );
|
||||||
|
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
CreateNewCampaignModal: () => <div>Mocked CreateNewCampaignModal</div>,
|
CreateNewCampaignModal: () => <div>Mocked CreateNewCampaignModal</div>,
|
||||||
} ) );
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a test campaign data object.
|
* Create a test campaign data object.
|
||||||
|
|
|
@ -24,9 +24,11 @@ import {
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { CardHeaderTitle } from '~/marketing/components';
|
import {
|
||||||
import { useCampaigns } from './useCampaigns';
|
CardHeaderTitle,
|
||||||
import { CreateNewCampaignModal } from './CreateNewCampaignModal';
|
CreateNewCampaignModal,
|
||||||
|
} from '~/marketing/components';
|
||||||
|
import { useCampaigns } from '~/marketing/hooks';
|
||||||
import './Campaigns.scss';
|
import './Campaigns.scss';
|
||||||
|
|
||||||
const tableCaption = __( 'Campaigns', 'woocommerce' );
|
const tableCaption = __( 'Campaigns', 'woocommerce' );
|
||||||
|
@ -139,7 +141,7 @@ export const Campaigns = () => {
|
||||||
{ el.title }
|
{ el.title }
|
||||||
</Link>
|
</Link>
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
{ el.description && (
|
{ !! el.description && (
|
||||||
<FlexItem className="woocommerce-marketing-campaigns-card__campaign-description">
|
<FlexItem className="woocommerce-marketing-campaigns-card__campaign-description">
|
||||||
{ el.description }
|
{ el.description }
|
||||||
</FlexItem>
|
</FlexItem>
|
||||||
|
@ -168,14 +170,14 @@ export const Campaigns = () => {
|
||||||
>
|
>
|
||||||
{ __( 'Create new campaign', 'woocommerce' ) }
|
{ __( 'Create new campaign', 'woocommerce' ) }
|
||||||
</Button>
|
</Button>
|
||||||
{ isModalOpen && (
|
{ !! isModalOpen && (
|
||||||
<CreateNewCampaignModal
|
<CreateNewCampaignModal
|
||||||
onRequestClose={ () => setModalOpen( false ) }
|
onRequestClose={ () => setModalOpen( false ) }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{ getContent() }
|
{ getContent() }
|
||||||
{ total && total > perPage && (
|
{ !! ( total && total > perPage ) && (
|
||||||
<CardFooter className="woocommerce-marketing-campaigns-card__footer">
|
<CardFooter className="woocommerce-marketing-campaigns-card__footer">
|
||||||
<Pagination
|
<Pagination
|
||||||
showPerPagePicker={ false }
|
showPerPagePicker={ false }
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Fragment, useState } from '@wordpress/element';
|
import {
|
||||||
|
Fragment,
|
||||||
|
useState,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
} from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
@ -32,11 +38,19 @@ type ChannelsProps = {
|
||||||
onInstalledAndActivated?: () => void;
|
onInstalledAndActivated?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Channels: React.FC< ChannelsProps > = ( {
|
export type ChannelsRef = {
|
||||||
registeredChannels,
|
/**
|
||||||
recommendedChannels,
|
* Scroll into the "Add channels" section in the card.
|
||||||
onInstalledAndActivated,
|
* The section will be expanded, and the "Add channels" button will be in focus.
|
||||||
} ) => {
|
*/
|
||||||
|
scrollIntoAddChannels: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Channels = forwardRef< ChannelsRef, ChannelsProps >(
|
||||||
|
(
|
||||||
|
{ registeredChannels, recommendedChannels, onInstalledAndActivated },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const hasRegisteredChannels = registeredChannels.length >= 1;
|
const hasRegisteredChannels = registeredChannels.length >= 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,6 +58,21 @@ export const Channels: React.FC< ChannelsProps > = ( {
|
||||||
* Initial state is expanded if there are no registered channels in first page load.
|
* Initial state is expanded if there are no registered channels in first page load.
|
||||||
*/
|
*/
|
||||||
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
|
const [ expanded, setExpanded ] = useState( ! hasRegisteredChannels );
|
||||||
|
const addChannelsButtonRef = useRef< HTMLButtonElement >( null );
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ( {
|
||||||
|
scrollIntoAddChannels: () => {
|
||||||
|
setExpanded( true );
|
||||||
|
addChannelsButtonRef.current?.focus();
|
||||||
|
addChannelsButtonRef.current?.scrollIntoView( {
|
||||||
|
block: 'center',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
} ),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="woocommerce-marketing-channels-card">
|
<Card className="woocommerce-marketing-channels-card">
|
||||||
|
@ -62,32 +91,35 @@ export const Channels: React.FC< ChannelsProps > = ( {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{ /* Registered channels section. */ }
|
{ /* Registered channels section. */ }
|
||||||
{ registeredChannels.map( ( el, idx ) => {
|
{ registeredChannels.map( ( el, idx ) => (
|
||||||
return (
|
|
||||||
<Fragment key={ el.slug }>
|
<Fragment key={ el.slug }>
|
||||||
<RegisteredChannelCardBody registeredChannel={ el } />
|
<RegisteredChannelCardBody registeredChannel={ el } />
|
||||||
{ idx !== registeredChannels.length - 1 && (
|
{ idx !== registeredChannels.length - 1 && (
|
||||||
<CardDivider />
|
<CardDivider />
|
||||||
) }
|
) }
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
) ) }
|
||||||
} ) }
|
|
||||||
|
|
||||||
{ /* Recommended channels section. */ }
|
{ /* Recommended channels section. */ }
|
||||||
{ recommendedChannels.length >= 1 && (
|
{ recommendedChannels.length >= 1 && (
|
||||||
<div>
|
<div>
|
||||||
{ hasRegisteredChannels && (
|
{ !! hasRegisteredChannels && (
|
||||||
<>
|
<>
|
||||||
<CardDivider />
|
<CardDivider />
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Button
|
<Button
|
||||||
|
ref={ addChannelsButtonRef }
|
||||||
variant="link"
|
variant="link"
|
||||||
onClick={ () => setExpanded( ! expanded ) }
|
onClick={ () =>
|
||||||
|
setExpanded( ! expanded )
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{ __( 'Add channels', 'woocommerce' ) }
|
{ __( 'Add channels', 'woocommerce' ) }
|
||||||
<Icon
|
<Icon
|
||||||
icon={
|
icon={
|
||||||
expanded ? chevronUp : chevronDown
|
expanded
|
||||||
|
? chevronUp
|
||||||
|
: chevronDown
|
||||||
}
|
}
|
||||||
size={ 24 }
|
size={ 24 }
|
||||||
/>
|
/>
|
||||||
|
@ -95,9 +127,8 @@ export const Channels: React.FC< ChannelsProps > = ( {
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</>
|
</>
|
||||||
) }
|
) }
|
||||||
{ expanded &&
|
{ !! expanded &&
|
||||||
recommendedChannels.map( ( el, idx ) => {
|
recommendedChannels.map( ( el, idx ) => (
|
||||||
return (
|
|
||||||
<Fragment key={ el.plugin }>
|
<Fragment key={ el.plugin }>
|
||||||
<SmartPluginCardBody
|
<SmartPluginCardBody
|
||||||
plugin={ el }
|
plugin={ el }
|
||||||
|
@ -110,10 +141,10 @@ export const Channels: React.FC< ChannelsProps > = ( {
|
||||||
<CardDivider />
|
<CardDivider />
|
||||||
) }
|
) }
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
) ) }
|
||||||
} ) }
|
|
||||||
</div>
|
</div>
|
||||||
) }
|
) }
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const RegisteredChannelCardBody: React.FC<
|
||||||
registeredChannel.description
|
registeredChannel.description
|
||||||
) : (
|
) : (
|
||||||
<div className="woocommerce-marketing-registered-channel-description">
|
<div className="woocommerce-marketing-registered-channel-description">
|
||||||
{ registeredChannel.syncStatus && (
|
{ !! registeredChannel.syncStatus && (
|
||||||
<>
|
<>
|
||||||
<SyncStatus status={ registeredChannel.syncStatus } />
|
<SyncStatus status={ registeredChannel.syncStatus } />
|
||||||
<div className="woocommerce-marketing-registered-channel-description__separator" />
|
<div className="woocommerce-marketing-registered-channel-description__separator" />
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export { Channels } from './Channels';
|
export { Channels } from './Channels';
|
||||||
|
export type { ChannelsRef } from './Channels';
|
||||||
|
|
|
@ -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';
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 0 256 153" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<title>WooCommerce Logo</title>
|
||||||
|
<metadata>
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||||
|
<dc:title/>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path d="m23.759 0h208.38c13.187 0 23.863 10.675 23.863 23.863v79.542c0 13.187-10.675 23.863-23.863 23.863h-74.727l10.257 25.118-45.109-25.118h-98.695c-13.187 0-23.863-10.675-23.863-23.863v-79.542c-0.10466-13.083 10.571-23.863 23.758-23.863z" fill="#7f54b3"/>
|
||||||
|
<path d="m14.578 21.75c1.4569-1.9772 3.6423-3.0179 6.5561-3.226 5.3073-0.41626 8.3252 2.0813 9.0537 7.4927 3.226 21.75 6.7642 40.169 10.511 55.259l22.79-43.395c2.0813-3.9545 4.6829-6.0358 7.8049-6.2439 4.5789-0.3122 7.3886 2.6016 8.5333 8.7415 2.6016 13.841 5.9317 25.6 9.8862 35.59 2.7057-26.433 7.2846-45.476 13.737-57.236 1.561-2.9138 3.8504-4.3707 6.8683-4.5789 2.3935-0.20813 4.5789 0.52033 6.5561 2.0813 1.9772 1.561 3.0179 3.5382 3.226 5.9317 0.10406 1.8732-0.20813 3.4341-1.0407 4.9951-4.0585 7.4927-7.3886 20.085-10.094 37.567-2.6016 16.963-3.5382 30.179-2.9138 39.649 0.20813 2.6016-0.20813 4.8911-1.2488 6.8683-1.2488 2.2894-3.122 3.5382-5.5154 3.7463-2.7057 0.20813-5.5154-1.0406-8.2211-3.8504-9.678-9.8862-17.379-24.663-22.998-44.332-6.7642 13.32-11.759 23.311-14.985 29.971-6.1398 11.759-11.343 17.795-15.714 18.107-2.8098 0.20813-5.2033-2.1854-7.2846-7.1805-5.3073-13.633-11.031-39.961-17.171-78.985-0.41626-2.7057 0.20813-5.0992 1.665-6.9724zm223.64 16.338c-3.7463-6.5561-9.2618-10.511-16.65-12.072-1.9772-0.41626-3.8504-0.62439-5.6195-0.62439-9.9902 0-18.107 5.2033-24.455 15.61-5.4114 8.8455-8.1171 18.628-8.1171 29.346 0 8.013 1.665 14.881 4.9951 20.605 3.7463 6.5561 9.2618 10.511 16.65 12.072 1.9772 0.41626 3.8504 0.62439 5.6195 0.62439 10.094 0 18.211-5.2033 24.455-15.61 5.4114-8.9496 8.1171-18.732 8.1171-29.45 0.10406-8.1171-1.665-14.881-4.9951-20.501zm-13.112 28.826c-1.4569 6.8683-4.0585 11.967-7.9089 15.402-3.0179 2.7057-5.8276 3.8504-8.4293 3.3301-2.4976-0.52033-4.5789-2.7057-6.1398-6.7642-1.2488-3.226-1.8732-6.452-1.8732-9.4699 0-2.6016 0.20813-5.2033 0.72846-7.5967 0.93659-4.2667 2.7057-8.4293 5.5154-12.384 3.4341-5.0992 7.0764-7.1805 10.823-6.452 2.4976 0.52033 4.5789 2.7057 6.1398 6.7642 1.2488 3.226 1.8732 6.452 1.8732 9.4699 0 2.7057-0.20813 5.3073-0.72846 7.7008zm-52.033-28.826c-3.7463-6.5561-9.3659-10.511-16.65-12.072-1.9772-0.41626-3.8504-0.62439-5.6195-0.62439-9.9902 0-18.107 5.2033-24.455 15.61-5.4114 8.8455-8.1171 18.628-8.1171 29.346 0 8.013 1.665 14.881 4.9951 20.605 3.7463 6.5561 9.2618 10.511 16.65 12.072 1.9772 0.41626 3.8504 0.62439 5.6195 0.62439 10.094 0 18.211-5.2033 24.455-15.61 5.4114-8.9496 8.1171-18.732 8.1171-29.45 0-8.1171-1.665-14.881-4.9951-20.501zm-13.216 28.826c-1.4569 6.8683-4.0585 11.967-7.9089 15.402-3.0179 2.7057-5.8276 3.8504-8.4293 3.3301-2.4976-0.52033-4.5789-2.7057-6.1398-6.7642-1.2488-3.226-1.8732-6.452-1.8732-9.4699 0-2.6016 0.20813-5.2033 0.72846-7.5967 0.93658-4.2667 2.7057-8.4293 5.5154-12.384 3.4341-5.0992 7.0764-7.1805 10.823-6.452 2.4976 0.52033 4.5789 2.7057 6.1398 6.7642 1.2488 3.226 1.8732 6.452 1.8732 9.4699 0.10406 2.7057-0.20813 5.3073-0.72846 7.7008z" fill="#fff"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
|
@ -27,7 +27,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => {
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<div className="woocommerce-marketing-learn-marketing-card__post-img">
|
<div className="woocommerce-marketing-learn-marketing-card__post-img">
|
||||||
{ post.image && <img src={ post.image } alt="" /> }
|
{ !! post.image && <img src={ post.image } alt="" /> }
|
||||||
</div>
|
</div>
|
||||||
<div className="woocommerce-marketing-learn-marketing-card__post-title">
|
<div className="woocommerce-marketing-learn-marketing-card__post-title">
|
||||||
{ post.title }
|
{ post.title }
|
||||||
|
@ -37,7 +37,7 @@ export const PostTile: React.FC< PostTileProps > = ( { post } ) => {
|
||||||
// translators: %s: author's name.
|
// translators: %s: author's name.
|
||||||
sprintf( __( 'By %s', 'woocommerce' ), post.author_name )
|
sprintf( __( 'By %s', 'woocommerce' ), post.author_name )
|
||||||
}
|
}
|
||||||
{ post.author_avatar && (
|
{ !! post.author_avatar && (
|
||||||
<img
|
<img
|
||||||
src={ post.author_avatar.replace( 's=96', 's=32' ) }
|
src={ post.author_avatar.replace( 's=96', 's=32' ) }
|
||||||
alt=""
|
alt=""
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { useRef } from '@wordpress/element';
|
||||||
import { useUser } from '@woocommerce/data';
|
import { useUser } from '@woocommerce/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,19 +11,28 @@ import '~/marketing/data';
|
||||||
import '~/marketing/data-multichannel';
|
import '~/marketing/data-multichannel';
|
||||||
import { CenteredSpinner } from '~/marketing/components';
|
import { CenteredSpinner } from '~/marketing/components';
|
||||||
import {
|
import {
|
||||||
|
useIntroductionBanner,
|
||||||
|
useCampaigns,
|
||||||
useRegisteredChannels,
|
useRegisteredChannels,
|
||||||
useRecommendedChannels,
|
useRecommendedChannels,
|
||||||
useCampaignTypes,
|
useCampaignTypes,
|
||||||
} from '~/marketing/hooks';
|
} from '~/marketing/hooks';
|
||||||
import { getAdminSetting } from '~/utils/admin-settings';
|
import { getAdminSetting } from '~/utils/admin-settings';
|
||||||
|
import { IntroductionBanner } from './IntroductionBanner';
|
||||||
import { Campaigns } from './Campaigns';
|
import { Campaigns } from './Campaigns';
|
||||||
import { Channels } from './Channels';
|
import { Channels, ChannelsRef } from './Channels';
|
||||||
import { InstalledExtensions } from './InstalledExtensions';
|
import { InstalledExtensions } from './InstalledExtensions';
|
||||||
import { DiscoverTools } from './DiscoverTools';
|
import { DiscoverTools } from './DiscoverTools';
|
||||||
import { LearnMarketing } from './LearnMarketing';
|
import { LearnMarketing } from './LearnMarketing';
|
||||||
import './MarketingOverviewMultichannel.scss';
|
import './MarketingOverviewMultichannel.scss';
|
||||||
|
|
||||||
export const MarketingOverviewMultichannel: React.FC = () => {
|
export const MarketingOverviewMultichannel: React.FC = () => {
|
||||||
|
const {
|
||||||
|
loading: loadingIntroductionBanner,
|
||||||
|
isIntroductionBannerDismissed,
|
||||||
|
dismissIntroductionBanner,
|
||||||
|
} = useIntroductionBanner();
|
||||||
|
const { loading: loadingCampaigns, meta: metaCampaigns } = useCampaigns();
|
||||||
const {
|
const {
|
||||||
loading: loadingCampaignTypes,
|
loading: loadingCampaignTypes,
|
||||||
data: dataCampaignTypes,
|
data: dataCampaignTypes,
|
||||||
|
@ -36,8 +46,11 @@ export const MarketingOverviewMultichannel: React.FC = () => {
|
||||||
const { loading: loadingRecommended, data: dataRecommended } =
|
const { loading: loadingRecommended, data: dataRecommended } =
|
||||||
useRecommendedChannels();
|
useRecommendedChannels();
|
||||||
const { currentUserCan } = useUser();
|
const { currentUserCan } = useUser();
|
||||||
|
const channelsRef = useRef< ChannelsRef >( null );
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
loadingIntroductionBanner ||
|
||||||
|
( loadingCampaigns && metaCampaigns?.total === undefined ) ||
|
||||||
( loadingCampaignTypes && ! dataCampaignTypes ) ||
|
( loadingCampaignTypes && ! dataCampaignTypes ) ||
|
||||||
( loadingRegistered && ! dataRegistered ) ||
|
( loadingRegistered && ! dataRegistered ) ||
|
||||||
( loadingRecommended && ! dataRecommended )
|
( loadingRecommended && ! dataRecommended )
|
||||||
|
@ -45,6 +58,11 @@ export const MarketingOverviewMultichannel: React.FC = () => {
|
||||||
return <CenteredSpinner />;
|
return <CenteredSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldShowCampaigns = !! (
|
||||||
|
dataRegistered?.length &&
|
||||||
|
( isIntroductionBannerDismissed || metaCampaigns?.total )
|
||||||
|
);
|
||||||
|
|
||||||
const shouldShowExtensions =
|
const shouldShowExtensions =
|
||||||
getAdminSetting( 'allowMarketplaceSuggestions', false ) &&
|
getAdminSetting( 'allowMarketplaceSuggestions', false ) &&
|
||||||
currentUserCan( 'install_plugins' );
|
currentUserCan( 'install_plugins' );
|
||||||
|
@ -56,18 +74,26 @@ export const MarketingOverviewMultichannel: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="woocommerce-marketing-overview-multichannel">
|
<div className="woocommerce-marketing-overview-multichannel">
|
||||||
{ !! dataRegistered?.length && <Campaigns /> }
|
{ ! isIntroductionBannerDismissed && (
|
||||||
{ dataRegistered &&
|
<IntroductionBanner
|
||||||
dataRecommended &&
|
onDismissClick={ dismissIntroductionBanner }
|
||||||
|
onAddChannelsClick={ () => {
|
||||||
|
channelsRef.current?.scrollIntoAddChannels();
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
{ shouldShowCampaigns && <Campaigns /> }
|
||||||
|
{ !! ( dataRegistered && dataRecommended ) &&
|
||||||
!! ( dataRegistered.length || dataRecommended.length ) && (
|
!! ( dataRegistered.length || dataRecommended.length ) && (
|
||||||
<Channels
|
<Channels
|
||||||
|
ref={ channelsRef }
|
||||||
registeredChannels={ dataRegistered }
|
registeredChannels={ dataRegistered }
|
||||||
recommendedChannels={ dataRecommended }
|
recommendedChannels={ dataRecommended }
|
||||||
onInstalledAndActivated={ refetch }
|
onInstalledAndActivated={ refetch }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
<InstalledExtensions />
|
<InstalledExtensions />
|
||||||
{ shouldShowExtensions && <DiscoverTools /> }
|
{ !! shouldShowExtensions && <DiscoverTools /> }
|
||||||
<LearnMarketing />
|
<LearnMarketing />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,7 +27,7 @@ const MarketingOverview = () => {
|
||||||
<WelcomeCard />
|
<WelcomeCard />
|
||||||
<InstalledExtensions />
|
<InstalledExtensions />
|
||||||
<MarketingOverviewSectionSlot />
|
<MarketingOverviewSectionSlot />
|
||||||
{ shouldShowExtensions && (
|
{ !! shouldShowExtensions && (
|
||||||
<RecommendedExtensions category="marketing" />
|
<RecommendedExtensions category="marketing" />
|
||||||
) }
|
) }
|
||||||
<KnowledgeBase category="marketing" />
|
<KnowledgeBase category="marketing" />
|
||||||
|
|
|
@ -193,7 +193,14 @@ const PaymentRecommendations: React.FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
),
|
),
|
||||||
before: (
|
before: (
|
||||||
<img src={ plugin.square_image || plugin.image } alt="" />
|
<img
|
||||||
|
src={
|
||||||
|
plugin.square_image ||
|
||||||
|
plugin.image_72x72 ||
|
||||||
|
plugin.image
|
||||||
|
}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
.woocommerce-product-block-editor {
|
||||||
|
.components-input-control {
|
||||||
|
&__prefix {
|
||||||
|
margin-left: $gap-smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__suffix {
|
||||||
|
margin-right: $gap-smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-currency-control {
|
||||||
|
.components-input-control__prefix {
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-input-control__input {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-product-form {
|
||||||
|
&__custom-label-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: $gap-smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__optional-input {
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-columns {
|
||||||
|
gap: $gap-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-woocommerce-product-section {
|
||||||
|
> .block-editor-inner-blocks > .block-editor-block-list__layout > .wp-block:not(:first-child) {
|
||||||
|
margin-top: $gap-large;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import {
|
||||||
} from '@woocommerce/product-editor';
|
} from '@woocommerce/product-editor';
|
||||||
import { Product } from '@woocommerce/data';
|
import { Product } from '@woocommerce/data';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { navigateTo } from '@woocommerce/navigation';
|
import { navigateTo, useConfirmUnsavedChanges } from '@woocommerce/navigation';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { useSelect } from '@wordpress/data';
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
|
@ -30,7 +30,6 @@ import { store } from '@wordpress/viewport';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import usePreventLeavingPage from '~/hooks/usePreventLeavingPage';
|
|
||||||
import './product-form-actions.scss';
|
import './product-form-actions.scss';
|
||||||
import { useProductMVPCESFooter } from '~/customer-effort-score-tracks/use-product-mvp-ces-footer';
|
import { useProductMVPCESFooter } from '~/customer-effort-score-tracks/use-product-mvp-ces-footer';
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ export const ProductFormActions: React.FC = () => {
|
||||||
const { isDirty, isValidForm, values, resetForm } =
|
const { isDirty, isValidForm, values, resetForm } =
|
||||||
useFormContext< Product >();
|
useFormContext< Product >();
|
||||||
|
|
||||||
usePreventLeavingPage( isDirty, preventLeavingProductForm );
|
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm );
|
||||||
|
|
||||||
useCustomerEffortScoreExitPageTracker(
|
useCustomerEffortScoreExitPageTracker(
|
||||||
! values.id ? 'new_product' : 'editing_new_product',
|
! values.id ? 'new_product' : 'editing_new_product',
|
||||||
|
|
|
@ -9,14 +9,16 @@ import { getAdminLink } from '@woocommerce/settings';
|
||||||
import { moreVertical } from '@wordpress/icons';
|
import { moreVertical } from '@wordpress/icons';
|
||||||
import { OPTIONS_STORE_NAME, Product } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME, Product } from '@woocommerce/data';
|
||||||
import { useFormContext } from '@woocommerce/components';
|
import { useFormContext } from '@woocommerce/components';
|
||||||
import { ALLOW_TRACKING_OPTION_NAME } from '@woocommerce/customer-effort-score';
|
import {
|
||||||
|
ALLOW_TRACKING_OPTION_NAME,
|
||||||
|
STORE_KEY as CES_STORE_KEY,
|
||||||
|
} from '@woocommerce/customer-effort-score';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ClassicEditorIcon } from './images/classic-editor-icon';
|
import { ClassicEditorIcon } from './images/classic-editor-icon';
|
||||||
import { FeedbackIcon } from './images/feedback-icon';
|
import { FeedbackIcon } from './images/feedback-icon';
|
||||||
import { STORE_KEY as CES_STORE_KEY } from '~/customer-effort-score-tracks/data/constants';
|
|
||||||
import { NEW_PRODUCT_MANAGEMENT } from '~/customer-effort-score-tracks/product-mvp-ces-footer';
|
import { NEW_PRODUCT_MANAGEMENT } from '~/customer-effort-score-tracks/product-mvp-ces-footer';
|
||||||
import './product-more-menu.scss';
|
import './product-more-menu.scss';
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,14 @@ import { useParams } from 'react-router-dom';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './product-page.scss';
|
import './product-page.scss';
|
||||||
|
import './product-block-page.scss';
|
||||||
|
|
||||||
declare const productBlockEditorSettings: ProductEditorSettings;
|
declare const productBlockEditorSettings: ProductEditorSettings;
|
||||||
|
|
||||||
const ProductEditor: React.FC< { product: Product | undefined } > = ( {
|
const ProductEditor: React.FC< { product: Product | undefined } > = ( {
|
||||||
product,
|
product,
|
||||||
} ) => {
|
} ) => {
|
||||||
if ( ! product ) {
|
if ( ! product?.id ) {
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,18 +35,23 @@ const ProductEditor: React.FC< { product: Product | undefined } > = ( {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditProductEditor: React.FC< { productId: string } > = ( {
|
const EditProductEditor: React.FC< { productId: number } > = ( {
|
||||||
productId,
|
productId,
|
||||||
} ) => {
|
} ) => {
|
||||||
const { product } = useSelect( ( select: typeof WPSelect ) => {
|
const { product } = useSelect(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
( select: typeof WPSelect ) => {
|
||||||
// @ts-ignore Missing types.
|
const { getEntityRecord } = select( 'core' );
|
||||||
const { getEditedEntityRecord } = select( 'core' );
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
product: getEditedEntityRecord( 'postType', 'product', productId ),
|
product: getEntityRecord(
|
||||||
|
'postType',
|
||||||
|
'product',
|
||||||
|
productId
|
||||||
|
) as Product,
|
||||||
};
|
};
|
||||||
} );
|
},
|
||||||
|
[ productId ]
|
||||||
|
);
|
||||||
|
|
||||||
return <ProductEditor product={ product } />;
|
return <ProductEditor product={ product } />;
|
||||||
};
|
};
|
||||||
|
@ -74,7 +80,9 @@ export default function ProductPage() {
|
||||||
const { productId } = useParams();
|
const { productId } = useParams();
|
||||||
|
|
||||||
if ( productId ) {
|
if ( productId ) {
|
||||||
return <EditProductEditor productId={ productId } />;
|
return (
|
||||||
|
<EditProductEditor productId={ Number.parseInt( productId, 10 ) } />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AddProductEditor />;
|
return <AddProductEditor />;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import { preventLeavingProductForm } from '@woocommerce/product-editor';
|
import { preventLeavingProductForm } from '@woocommerce/product-editor';
|
||||||
import { registerPlugin } from '@wordpress/plugins';
|
import { registerPlugin } from '@wordpress/plugins';
|
||||||
import { useDispatch } from '@wordpress/data';
|
import { useDispatch } from '@wordpress/data';
|
||||||
|
import { useConfirmUnsavedChanges } from '@woocommerce/navigation';
|
||||||
import { useFormContext } from '@woocommerce/components';
|
import { useFormContext } from '@woocommerce/components';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
|
@ -18,7 +19,6 @@ import { useState } from '@wordpress/element';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import usePreventLeavingPage from '~/hooks/usePreventLeavingPage';
|
|
||||||
import './product-form-actions.scss';
|
import './product-form-actions.scss';
|
||||||
|
|
||||||
export const ProductVariationFormActions: React.FC = () => {
|
export const ProductVariationFormActions: React.FC = () => {
|
||||||
|
@ -31,7 +31,7 @@ export const ProductVariationFormActions: React.FC = () => {
|
||||||
const { createNotice } = useDispatch( 'core/notices' );
|
const { createNotice } = useDispatch( 'core/notices' );
|
||||||
const [ isSaving, setIsSaving ] = useState( false );
|
const [ isSaving, setIsSaving ] = useState( false );
|
||||||
|
|
||||||
usePreventLeavingPage( isDirty, preventLeavingProductForm );
|
useConfirmUnsavedChanges( isDirty, preventLeavingProductForm );
|
||||||
|
|
||||||
const onSave = async () => {
|
const onSave = async () => {
|
||||||
setIsSaving( true );
|
setIsSaving( true );
|
||||||
|
|
|
@ -53,7 +53,10 @@ jest.mock( '@woocommerce/product-editor', () => {
|
||||||
} ),
|
} ),
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
jest.mock( '~/hooks/usePreventLeavingPage' );
|
jest.mock( '@woocommerce/navigation', () => ( {
|
||||||
|
...jest.requireActual( '@woocommerce/navigation' ),
|
||||||
|
useConfirmUnsavedChanges: jest.fn(),
|
||||||
|
} ) );
|
||||||
jest.mock( '@woocommerce/customer-effort-score', () => ( {
|
jest.mock( '@woocommerce/customer-effort-score', () => ( {
|
||||||
useCustomerEffortScoreExitPageTracker: jest.fn(),
|
useCustomerEffortScoreExitPageTracker: jest.fn(),
|
||||||
} ) );
|
} ) );
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
|
||||||
settingsUrl: manageUrl,
|
settingsUrl: manageUrl,
|
||||||
is_local_partner: isLocalPartner,
|
is_local_partner: isLocalPartner,
|
||||||
external_link: externalLink,
|
external_link: externalLink,
|
||||||
|
transaction_processors: transactionProcessors,
|
||||||
} = paymentGateway;
|
} = paymentGateway;
|
||||||
|
|
||||||
const connectSlot = useSlot(
|
const connectSlot = useSlot(
|
||||||
|
@ -88,6 +89,21 @@ export const Item = ( { isRecommended, markConfigured, paymentGateway } ) => {
|
||||||
<div className="woocommerce-task-payment__content">
|
<div className="woocommerce-task-payment__content">
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
|
{ transactionProcessors && (
|
||||||
|
<div className="woocommerce-task-payment__transaction-processors_images">
|
||||||
|
{ Object.keys( transactionProcessors ).map(
|
||||||
|
( key ) => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={ transactionProcessors[ key ] }
|
||||||
|
alt={ key }
|
||||||
|
key={ key }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
<div className="woocommerce-task-payment__footer">
|
<div className="woocommerce-task-payment__footer">
|
||||||
<Action
|
<Action
|
||||||
|
|
|
@ -67,6 +67,18 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.woocommerce-task-payment__transaction-processors_images {
|
||||||
|
padding-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.woocommerce-task-payment__description {
|
.woocommerce-task-payment__description {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue