Merge branch 'trunk' into e2e/fix-failing-daily-product-variations

This commit is contained in:
Jonathan Lane 2023-03-20 11:00:13 -06:00 committed by GitHub
commit ab75a00a83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
254 changed files with 5947 additions and 1709 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &amp 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**

View File

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

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update showOtherPaymentMethods() to test latest payment task properly

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add CES data store to @woocommerce/customer-effort-score

View File

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

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Add tracks for plugin actions and handle plugin error properly

View File

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

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add hook to check unsaved form changes before page navigation

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a product header component to the blocks interface

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Improve accessibility around product editor tabs

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add tests around product block editor tabs

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new pricing block to the product editor package.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove the product block breadcrumbs and sidebar inspector

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update use of blocks within block editor to always make use of template.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,4 +3,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 $gap; padding: 0 $gap;
&__actions {
margin-left: auto;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
]
: ''; : '';
}; };

View File

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

View File

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

View File

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

View File

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

View File

@ -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( () =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -64,7 +64,7 @@ const CollapsibleCard: React.FC< CollapsibleCardProps > = ( {
{ ! collapsed && ( { ! collapsed && (
<> <>
{ children } { children }
{ footer && <CardFooter>{ footer }</CardFooter> } { !! footer && <CardFooter>{ footer }</CardFooter> }
</> </>
) } ) }
</Card> </Card>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
export { Channels } from './Channels'; export { Channels } from './Channels';
export type { ChannelsRef } from './Channels';

View File

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

View File

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

View File

@ -0,0 +1 @@
export { IntroductionBanner } from './IntroductionBanner';

View File

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

View File

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

View File

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

View File

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

View File

@ -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=""
/>
), ),
}; };
} ); } );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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