Merge branch 'trunk' into hpos/unit-tests

This commit is contained in:
Vedanshu Jain 2023-03-17 13:39:19 +05:30
commit ade1540ece
246 changed files with 6402 additions and 2226 deletions

View File

@ -29,7 +29,7 @@ runs:
- name: Setup PNPM
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
with:
version: '^7.22.0'
version: '7.29.1'
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c

View File

@ -31,9 +31,9 @@ jobs:
include:
- wp: nightly
php: '7.4'
- wp: '5.9'
- wp: '6.0'
php: 7.4
- wp: '5.8'
- wp: '5.9'
php: 7.4
services:
database:

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

@ -51,7 +51,7 @@
"sass": "^1.49.9",
"sass-loader": "^10.2.1",
"syncpack": "^9.8.4",
"turbo": "^1.7.0",
"turbo": "^1.8.3",
"typescript": "^4.8.3",
"url-loader": "^1.1.2",
"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 > {
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' );
}

View File

@ -0,0 +1 @@
package-lock=false

View File

@ -0,0 +1,11 @@
# Admin Layout
A collection of WooCommerce Admin layout components and utilities.
## Installation
Install the module
```bash
pnpm install @woocommerce/product-editor --save
```

View File

@ -0,0 +1,3 @@
# Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Create @woocommerce/admin-layout package to house header, footer, and similar components and utilities.

View File

@ -0,0 +1,32 @@
{
"name": "woocommerce/admin-layout",
"description": "WooCommerce Admin layout component library",
"type": "library",
"license": "GPL-3.0-or-later",
"minimum-stability": "dev",
"require-dev": {
"automattic/jetpack-changelogger": "3.3.0"
},
"config": {
"platform": {
"php": "7.2"
}
},
"extra": {
"changelogger": {
"formatter": {
"filename": "../../../tools/changelogger/class-package-formatter.php"
},
"types": {
"fix": "Fixes an existing bug",
"add": "Adds functionality",
"update": "Update existing functionality",
"dev": "Development related task",
"tweak": "A minor adjustment to the codebase",
"performance": "Address performance issues",
"enhancement": "Improve existing functionality"
},
"changelog": "CHANGELOG.md"
}
}
}

483
packages/js/admin-layout/composer.lock generated Normal file
View File

@ -0,0 +1,483 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "5ce7bfd856ef579554b96ae2f7451072",
"packages": [],
"packages-dev": [
{
"name": "automattic/jetpack-changelogger",
"version": "v3.3.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-changelogger.git",
"reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-changelogger/zipball/8f63c829b8d1b0d7b1d5de93510d78523ed18959",
"reference": "8f63c829b8d1b0d7b1d5de93510d78523ed18959",
"shasum": ""
},
"require": {
"php": ">=5.6",
"symfony/console": "^3.4 || ^5.2 || ^6.0",
"symfony/process": "^3.4 || ^5.2 || ^6.0",
"wikimedia/at-ease": "^1.2 || ^2.0"
},
"require-dev": {
"wikimedia/testing-access-wrapper": "^1.0 || ^2.0",
"yoast/phpunit-polyfills": "1.0.4"
},
"bin": [
"bin/changelogger"
],
"type": "project",
"extra": {
"autotagger": true,
"branch-alias": {
"dev-trunk": "3.3.x-dev"
},
"mirror-repo": "Automattic/jetpack-changelogger",
"version-constants": {
"::VERSION": "src/Application.php"
},
"changelogger": {
"link-template": "https://github.com/Automattic/jetpack-changelogger/compare/${old}...${new}"
}
},
"autoload": {
"psr-4": {
"Automattic\\Jetpack\\Changelog\\": "lib",
"Automattic\\Jetpack\\Changelogger\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"description": "Jetpack Changelogger tool. Allows for managing changelogs by dropping change files into a changelog directory with each PR.",
"support": {
"source": "https://github.com/Automattic/jetpack-changelogger/tree/v3.3.0"
},
"time": "2022-12-26T13:49:01+00:00"
},
{
"name": "psr/log",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/1.1.4"
},
"time": "2021-05-03T11:20:27+00:00"
},
{
"name": "symfony/console",
"version": "3.4.x-dev",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/a10b1da6fc93080c180bba7219b5ff5b7518fe81",
"reference": "a10b1da6fc93080c180bba7219b5ff5b7518fe81",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/debug": "~2.8|~3.0|~4.0",
"symfony/polyfill-mbstring": "~1.0"
},
"conflict": {
"symfony/dependency-injection": "<3.4",
"symfony/process": "<3.3"
},
"provide": {
"psr/log-implementation": "1.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/config": "~3.3|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/event-dispatcher": "~2.8|~3.0|~4.0",
"symfony/lock": "~3.4|~4.0",
"symfony/process": "~3.3|~4.0"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/lock": "",
"symfony/process": ""
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/console/tree/3.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
},
{
"name": "symfony/debug",
"version": "4.4.x-dev",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "1a692492190773c5310bc7877cb590c04c2f05be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be",
"reference": "1a692492190773c5310bc7877cb590c04c2f05be",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/http-kernel": "<3.4"
},
"require-dev": {
"symfony/http-kernel": "^3.4|^4.0|^5.0"
},
"default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Debug\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools to ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/debug/tree/v4.4.44"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"abandoned": "symfony/error-handler",
"time": "2022-07-28T16:29:46+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f9c7affe77a00ae32ca127ca6833d034e6d33f25",
"reference": "f9c7affe77a00ae32ca127ca6833d034e6d33f25",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.28-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/main"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-01-30T17:25:47+00:00"
},
{
"name": "symfony/process",
"version": "3.4.x-dev",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/b8648cf1d5af12a44a51d07ef9bf980921f15fca",
"reference": "b8648cf1d5af12a44a51d07ef9bf980921f15fca",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/3.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
},
{
"name": "wikimedia/at-ease",
"version": "v2.0.0",
"source": {
"type": "git",
"url": "https://github.com/wikimedia/at-ease.git",
"reference": "013ac61929797839c80a111a3f1a4710d8248e7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wikimedia/at-ease/zipball/013ac61929797839c80a111a3f1a4710d8248e7a",
"reference": "013ac61929797839c80a111a3f1a4710d8248e7a",
"shasum": ""
},
"require": {
"php": ">=5.6.99"
},
"require-dev": {
"jakub-onderka/php-console-highlighter": "0.3.2",
"jakub-onderka/php-parallel-lint": "1.0.0",
"mediawiki/mediawiki-codesniffer": "22.0.0",
"mediawiki/minus-x": "0.3.1",
"ockcyp/covers-validator": "0.5.1 || 0.6.1",
"phpunit/phpunit": "4.8.36 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/Wikimedia/Functions.php"
],
"psr-4": {
"Wikimedia\\AtEase\\": "src/Wikimedia/AtEase/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"GPL-2.0-or-later"
],
"authors": [
{
"name": "Tim Starling",
"email": "tstarling@wikimedia.org"
},
{
"name": "MediaWiki developers",
"email": "wikitech-l@lists.wikimedia.org"
}
],
"description": "Safe replacement to @ for suppressing warnings.",
"homepage": "https://www.mediawiki.org/wiki/at-ease",
"support": {
"source": "https://github.com/wikimedia/at-ease/tree/master"
},
"time": "2018-10-10T15:39:06+00:00"
}
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.2"
},
"plugin-api-version": "2.3.0"
}

View File

@ -0,0 +1,72 @@
{
"name": "@woocommerce/admin-layout",
"version": "1.0.0-beta.0",
"description": "WooCommerce admin layout copmonents and utilities.",
"author": "Automattic",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"woocommerce"
],
"homepage": "https://github.com/woocommerce/woocommerce/tree/trunk/packages/js/admin-layout/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
},
"bugs": {
"url": "https://github.com/woocommerce/woocommerce/issues"
},
"main": "build/index.js",
"module": "build-module/index.js",
"types": "build-types",
"react-native": "src/index",
"sideEffects": [
"build-style/**",
"src/**/*.scss"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"turbo:build": "pnpm run build:js && pnpm run build:css",
"prepare": "composer install",
"changelog": "composer exec -- changelogger",
"clean": "pnpm exec rimraf tsconfig.tsbuildinfo build build-*",
"build": "pnpm -w exec turbo run turbo:build --filter=$npm_package_name",
"lint": "eslint src",
"build:js": "tsc --build ./tsconfig.json ./tsconfig-cjs.json",
"build:css": "webpack",
"start": "concurrently \"tsc --build --watch\" \"webpack --watch\"",
"prepack": "pnpm run clean && pnpm run build",
"lint:fix": "eslint src --fix"
},
"devDependencies": {
"@types/react": "^17.0.2",
"@types/wordpress__components": "^19.10.3",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/browserslist-config": "wp-6.0",
"css-loader": "^3.6.0",
"eslint": "^8.32.0",
"jest": "^27.5.1",
"jest-cli": "^27.5.1",
"postcss-loader": "^4.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass-loader": "^10.2.1",
"ts-jest": "^27.1.3",
"typescript": "^4.8.3",
"webpack": "^5.70.0",
"webpack-cli": "^3.3.12"
},
"peerDependencies": {
"@types/react": "^17.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"dependencies": {
"@woocommerce/components": "workspace:*",
"@wordpress/components": "wp-6.0",
"@wordpress/element": "wp-6.0"
}
}

View File

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

View File

@ -0,0 +1,10 @@
export { WC_FOOTER_SLOT_NAME, WooFooterItem } from './woo-footer-item';
export { WC_HEADER_SLOT_NAME, WooHeaderItem } from './woo-header-item';
export {
WC_HEADER_NAVIGATION_SLOT_NAME,
WooHeaderNavigationItem,
} from './woo-header-navigation-item';
export {
WC_HEADER_PAGE_TITLE_SLOT_NAME,
WooHeaderPageTitle,
} from './woo-header-page-title';

View File

@ -1,14 +1,16 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
/**
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '~/utils';
import { createElement } from '@wordpress/element';
import {
createOrderedChildren,
sortFillsByOrder,
} from '@woocommerce/components';
export const WC_FOOTER_SLOT_NAME = 'woocommerce_footer_item';
/**
* Create a Fill for extensions to add items to the WooCommerce Admin footer.
*
@ -27,7 +29,10 @@ export const WC_FOOTER_SLOT_NAME = 'woocommerce_footer_item';
* @param {Array} param0.children - Node children.
* @param {Array} param0.order - Node order.
*/
export const WooFooterItem: React.FC< { order?: number } > & {
export const WooFooterItem: React.FC< {
children?: React.ReactNode;
order?: number;
} > & {
Slot: React.FC< Slot.Props >;
} = ( { children, order = 1 } ) => {
return (

View File

@ -0,0 +1,51 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { createElement } from '@wordpress/element';
import {
createOrderedChildren,
sortFillsByOrder,
} from '@woocommerce/components';
export const WC_HEADER_SLOT_NAME = 'woocommerce_header_item';
/**
* Create a Fill for extensions to add items to the WooCommerce Admin header.
*
* @slotFill WooHeaderItem
* @scope woocommerce-admin
* @example
* const MyHeaderItem = () => (
* <WooHeaderItem>My header item</WooHeaderItem>
* );
*
* registerPlugin( 'my-extension', {
* render: MyHeaderItem,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
* @param {Array} param0.order - Node order.
*/
export const WooHeaderItem: React.FC< {
children?: React.ReactNode;
order?: number;
} > & {
Slot: React.FC< Slot.Props >;
} = ( { children, order = 1 } ) => {
return (
<Fill name={ WC_HEADER_SLOT_NAME }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren( children, order, fillProps );
} }
</Fill>
);
};
WooHeaderItem.Slot = ( { fillProps } ) => (
<Slot name={ WC_HEADER_SLOT_NAME } fillProps={ fillProps }>
{ sortFillsByOrder }
</Slot>
);

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { createElement } from '@wordpress/element';
import {
createOrderedChildren,
sortFillsByOrder,
} from '@woocommerce/components';
export const WC_HEADER_NAVIGATION_SLOT_NAME =
'woocommerce_header_navigation_item';
/**
* Create a Fill for extensions to add items to the WooCommerce Admin
* navigation area left of the page title.
*
* @slotFill WooHeaderNavigationItem
* @scope woocommerce-admin
* @example
* const MyNavigationItem = () => (
* <WooHeaderNavigationItem>My nav item</WooHeaderNavigationItem>
* );
*
* registerPlugin( 'my-extension', {
* render: MyNavigationItem,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
* @param {Array} param0.order - Node order.
*/
export const WooHeaderNavigationItem: React.FC< {
children?: React.ReactNode;
order?: number;
} > & {
Slot: React.FC< Slot.Props >;
} = ( { children, order = 1 } ) => {
return (
<Fill name={ WC_HEADER_NAVIGATION_SLOT_NAME }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren( children, order, fillProps );
} }
</Fill>
);
};
WooHeaderNavigationItem.Slot = ( { fillProps }: Slot.Props ) => (
<Slot name={ WC_HEADER_NAVIGATION_SLOT_NAME } fillProps={ fillProps }>
{ sortFillsByOrder }
</Slot>
);

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { createElement, Fragment } from '@wordpress/element';
export const WC_HEADER_PAGE_TITLE_SLOT_NAME = 'woocommerce_header_page_title';
/**
* Create a Fill for extensions to add custom page titles.
*
* @slotFill WooHeaderPageTitle
* @scope woocommerce-admin
* @example
* const MyPageTitle = () => (
* <WooHeaderPageTitle>My page title</WooHeaderPageTitle>
* );
*
* registerPlugin( 'my-page-title', {
* render: MyPageTitle,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
*/
export const WooHeaderPageTitle: React.FC< {
children?: React.ReactNode;
} > & {
Slot: React.FC< Slot.Props >;
} = ( { children } ) => {
return <Fill name={ WC_HEADER_PAGE_TITLE_SLOT_NAME }>{ children }</Fill>;
};
WooHeaderPageTitle.Slot = ( { fillProps } ) => (
<Slot name={ WC_HEADER_PAGE_TITLE_SLOT_NAME } fillProps={ fillProps }>
{ ( fills ) => {
return <>{ [ ...fills ].pop() }</>;
} }
</Slot>
);

View File

View File

@ -0,0 +1,6 @@
{
"extends": "../tsconfig-cjs",
"compilerOptions": {
"outDir": "build"
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"rootDir": "src",
"outDir": "build-module",
"declaration": true,
"declarationMap": true,
"declarationDir": "./build-types"
}
}

View File

@ -0,0 +1,18 @@
/**
* Internal dependencies
*/
const { webpackConfig } = require( '@woocommerce/internal-style-build' );
module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: {
'build-style': __dirname + '/src/style.scss',
},
output: {
path: __dirname,
},
module: {
rules: webpackConfig.rules,
},
plugins: webpackConfig.plugins,
};

View File

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

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Add additional components to package.

View File

@ -47,8 +47,10 @@
"@types/testing-library__jest-dom": "^5.14.3",
"@types/wordpress__components": "^19.10.3",
"@types/wordpress__data": "^6.0.0",
"@woocommerce/data": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@woocommerce/navigation": "workspace:*",
"@wordpress/browserslist-config": "wp-6.0",
"concurrently": "^7.0.0",
"css-loader": "^3.6.0",

View File

@ -9,7 +9,7 @@ import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { CustomerFeedbackModal } from './customer-feedback-modal';
import { CustomerFeedbackModal } from '../customer-feedback-modal';
const noop = () => {};

View File

@ -8,7 +8,7 @@ import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { CustomerEffortScore } from '../customer-effort-score';
import { CustomerEffortScore } from '..';
const noop = () => {};

View File

@ -0,0 +1 @@
export const ALLOW_TRACKING_OPTION_NAME = 'woocommerce_allow_tracking';

View File

@ -10,7 +10,7 @@ import {
addCustomerEffortScoreExitPageListener,
addExitPage,
removeCustomerEffortScoreExitPageListener,
} from './customer-effort-score-exit-page';
} from '../../utils/customer-effort-score-exit-page';
export const useCustomerEffortScoreExitPageTracker = (
pageId: string,

View File

@ -1,5 +1,9 @@
export * from './customer-effort-score';
export * from './customer-feedback-simple';
export * from './customer-feedback-modal';
export * from './product-mvp-feedback-modal';
export * from './feedback-modal';
export * from './components/customer-effort-score';
export * from './components/customer-feedback-simple';
export * from './components/customer-feedback-modal';
export * from './components/product-mvp-feedback-modal';
export * from './components/feedback-modal';
export * from './hooks/use-customer-effort-score-exit-page-tracker';
export * from './store';
export * from './utils/customer-effort-score-exit-page';
export * from './constants';

View File

@ -11,7 +11,9 @@ import * as actions from './actions';
import * as resolvers from './resolvers';
import * as selectors from './selectors';
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, {
actions,

View File

@ -1,6 +1,6 @@
@import 'customer-feedback-simple/customer-feedback-simple.scss';
@import 'product-mvp-feedback-modal/product-mvp-feedback-modal.scss';
@import 'feedback-modal/feedback-modal.scss';
@import 'components/customer-feedback-simple/customer-feedback-simple.scss';
@import 'components/product-mvp-feedback-modal/product-mvp-feedback-modal.scss';
@import 'components/feedback-modal/feedback-modal.scss';
.woocommerce-customer-effort-score__selection {
margin: 1em 0 1.5em 0;

View File

@ -9,7 +9,13 @@ import { getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { ALLOW_TRACKING_OPTION_NAME } from './constants';
import { ALLOW_TRACKING_OPTION_NAME } from '../constants';
interface AdminWindow extends Window {
pagenow?: string;
adminpage?: string;
}
declare let window: AdminWindow;
const CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY = 'customer-effort-score-exit-page';
@ -87,7 +93,7 @@ const eventListeners: Record< string, ( event: BeforeUnloadEvent ) => void > =
/**
* Adds unload event listener to add pageId to exit page list incase there were unsaved changes.
*
* @param {string} pageId the page id of the page being exited early.
* @param {string} pageId the page id of the page being exited early.
* @param {Function} hasUnsavedChanges callback to check if the page had unsaved changes.
*/
export const addCustomerEffortScoreExitPageListener = (

View File

@ -1,5 +1,6 @@
module.exports = [
// wc-admin packages
'@woocommerce/admin-layout',
'@woocommerce/components',
'@woocommerce/csv-export',
'@woocommerce/currency',

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add @woocommerce/admin-layout package.

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/element": "wp-6.0",
"@wordpress/hooks": "wp-6.0",
"@wordpress/i18n": "wp-6.0",
"@wordpress/notices": "wp-6.0",
"@wordpress/url": "wp-6.0",
"history": "^5.3.0",

View File

@ -1,16 +1,17 @@
/**
* External dependencies
*/
import { useContext, useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { parseAdminUrl } from '@woocommerce/navigation';
import {
Location,
UNSAFE_NavigationContext as NavigationContext,
useLocation,
} from 'react-router-dom';
import { Location } from 'react-router-dom';
import { useEffect, useMemo } from '@wordpress/element';
export default function usePreventLeavingPage(
/**
* Internal dependencies
*/
import { getHistory } from '../history';
import { parseAdminUrl } from '../';
export const useConfirmUnsavedChanges = (
hasUnsavedChanges: 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
*/
message?: string
) {
) => {
const confirmMessage = useMemo(
() =>
message ??
__( 'Changes you made may not be saved.', 'woocommerce' ),
[ message ]
);
const { navigator } = useContext( NavigationContext );
const fromUrl = useLocation();
const history = getHistory();
// This effect prevent react router from navigate and show
// a confirmation message. It's a work around to beforeunload
// because react router does not triggers that event.
useEffect( () => {
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;
if (
typeof shouldConfirm === 'function' &&
@ -54,10 +55,10 @@ export default function usePreventLeavingPage(
};
return () => {
navigator.push = push;
history.push = push;
};
}
}, [ navigator, hasUnsavedChanges, confirmMessage ] );
}, [ history, hasUnsavedChanges, confirmMessage ] );
// This effect listen to the native beforeunload event to show
// a confirmation message
@ -79,4 +80,4 @@ export default function usePreventLeavingPage(
};
}
}, [ hasUnsavedChanges, confirmMessage ] );
}
};

View File

@ -18,9 +18,6 @@ import { getAdminLink } from '@woocommerce/settings';
* Internal dependencies
*/
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.
export { getHistory };
@ -28,6 +25,9 @@ export { getHistory };
// Export all filter utilities
export * from './filters';
// Export all hooks
export { useConfirmUnsavedChanges } from './hooks/use-confirm-unsaved-changes';
const TIME_EXCLUDED_SCREENS_FILTER = 'woocommerce_admin_time_excluded_screens';
/**
@ -79,7 +79,7 @@ export function getNewPath(
* @param {Object} query Query containing the parameters.
* @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.
*
@ -226,7 +226,7 @@ export function getIdsFromQuery( queryString = '' ) {
* @param {Object} query Query object.
* @return {Array} List of search words.
*/
export function getSearchWords( query = navUtils.getQuery() ) {
export function getSearchWords( query = getQuery() ) {
if ( typeof query !== 'object' ) {
throw new Error(
'Invalid parameter passed to getSearchWords, it expects an object or no parameters.'

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add section block for use in product editor

View File

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

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: tweak
Export the ProductEditorSettings type.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Suppress errant TS lint errors.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding name field block to product editor.

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

@ -34,20 +34,23 @@
"@woocommerce/components": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/data": "workspace:^4.1.0",
"@woocommerce/navigation": "workspace:^8.1.0",
"@woocommerce/number": "workspace:*",
"@woocommerce/tracks": "workspace:^1.3.0",
"@wordpress/block-editor": "^9.8.0",
"@wordpress/blocks": "^12.3.0",
"@wordpress/data": "wp-6.0",
"@wordpress/interface": "wp-6.0",
"@wordpress/keyboard-shortcuts": "wp-6.0",
"@wordpress/media-utils": "wp-6.0",
"@wordpress/components": "wp-6.0",
"@wordpress/compose": "wp-6.0",
"@wordpress/core-data": "wp-6.0",
"@wordpress/data": "wp-6.0",
"@wordpress/editor": "wp-6.0",
"@wordpress/element": "wp-6.0",
"@wordpress/html-entities": "wp-6.0",
"@wordpress/i18n": "wp-6.0",
"@wordpress/icons": "wp-6.0",
"@wordpress/interface": "wp-6.0",
"@wordpress/keyboard-shortcuts": "wp-6.0",
"@wordpress/media-utils": "wp-6.0",
"@wordpress/url": "wp-6.0",
"classnames": "^2.3.1",
"lodash": "^4.17.21",
@ -57,9 +60,12 @@
"@testing-library/react": "^12.1.3",
"@types/react": "^17.0.2",
"@types/wordpress__block-editor": "^7.0.0",
"@types/wordpress__block-library": "^2.6.1",
"@types/wordpress__blocks": "^11.0.7",
"@types/wordpress__components": "^19.10.3",
"@types/wordpress__core-data": "^2.4.5",
"@types/wordpress__data": "^6.0.2",
"@types/wordpress__editor": "^13.0.0",
"@types/wordpress__media-utils": "^3.0.0",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",

View File

@ -1,47 +1,62 @@
/**
* External dependencies
*/
import { BlockInstance } from '@wordpress/blocks';
import { createElement, useState, useMemo } from '@wordpress/element';
import { synchronizeBlocksWithTemplate, Template } from '@wordpress/blocks';
import {
createElement,
useMemo,
useLayoutEffect,
useState,
} from '@wordpress/element';
import { Product } from '@woocommerce/data';
import { useSelect, select as WPSelect } from '@wordpress/data';
import { uploadMedia } from '@wordpress/media-utils';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
BlockBreadcrumb,
BlockContextProvider,
BlockEditorKeyboardShortcuts,
BlockEditorProvider,
BlockList,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
BlockTools,
BlockInspector,
EditorSettings,
EditorBlockListSettings,
WritingFlow,
ObserveTyping,
} from '@wordpress/block-editor';
// It doesn't seem to notice the External dependency block whn @ts-ignore is added.
// eslint-disable-next-line @woocommerce/dependency-group
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore store should be included.
useEntityBlockEditor,
} from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { Sidebar } from '../sidebar';
import { Tabs } from '../tabs';
type BlockEditorProps = {
product: Partial< Product >;
settings: Partial< EditorSettings & EditorBlockListSettings > | undefined;
settings:
| ( Partial< EditorSettings & EditorBlockListSettings > & {
template?: Template[];
} )
| undefined;
};
export function BlockEditor( { settings: _settings }: BlockEditorProps ) {
const [ blocks, updateBlocks ] = useState< BlockInstance[] >();
export function BlockEditor( {
settings: _settings,
product,
}: BlockEditorProps ) {
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
const canUserCreateMedia = useSelect( ( select: typeof WPSelect ) => {
const { canUser } = select( 'core' ) as Record<
string,
( ...args: string[] ) => boolean
>;
return canUser( 'create', 'media' ) !== false;
const { canUser } = select( 'core' );
return canUser( 'create', 'media', '' ) !== false;
}, [] );
const settings = useMemo( () => {
@ -68,47 +83,47 @@ export function BlockEditor( { settings: _settings }: BlockEditorProps ) {
};
}, [ canUserCreateMedia, _settings ] );
/**
* Wrapper for updating blocks. Required as `onInput` callback passed to
* `BlockEditorProvider` is now called with more than 1 argument. Therefore
* attempting to setState directly via `updateBlocks` will trigger an error
* in React.
*
* @param _blocks
*/
function handleUpdateBlocks( _blocks: BlockInstance[] ) {
updateBlocks( _blocks );
}
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'postType',
'product',
{ id: product.id }
);
function handlePersistBlocks( newBlocks: BlockInstance[] ) {
updateBlocks( newBlocks );
useLayoutEffect( () => {
onChange(
synchronizeBlocksWithTemplate( [], _settings?.template ),
{}
);
}, [] );
if ( ! blocks ) {
return null;
}
return (
<div className="woocommerce-product-block-editor">
<BlockEditorProvider
value={ blocks }
onInput={ handleUpdateBlocks }
onChange={ handlePersistBlocks }
settings={ settings }
>
<BlockBreadcrumb />
<Sidebar.InspectorFill>
<BlockInspector />
</Sidebar.InspectorFill>
<div className="editor-styles-wrapper">
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore No types for this exist yet. */ }
<BlockEditorKeyboardShortcuts.Register />
<BlockTools>
<WritingFlow>
<ObserveTyping>
<BlockList className="woocommerce-product-block-editor__block-list" />
</ObserveTyping>
</WritingFlow>
</BlockTools>
</div>
</BlockEditorProvider>
<BlockContextProvider value={ { selectedTab } }>
<BlockEditorProvider
value={ blocks }
onInput={ onInput }
onChange={ onChange }
settings={ settings }
>
<Tabs onChange={ setSelectedTab } />
<div className="editor-styles-wrapper">
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore No types for this exist yet. */ }
<BlockEditorKeyboardShortcuts.Register />
<BlockTools>
<WritingFlow>
<ObserveTyping>
<BlockList className="woocommerce-product-block-editor__block-list" />
</ObserveTyping>
</WritingFlow>
</BlockTools>
</div>
</BlockEditorProvider>
</BlockContextProvider>
</div>
);
}

View File

@ -1,4 +1,15 @@
.woocommerce-product-block-editor {
h1,
h2,
h3,
h4,
h5,
h6,
p,
input {
font-family: var(--wp--preset--font-family--system-font);
}
.editor-styles-wrapper {
max-width: 650px;
margin-left: auto;

View File

@ -0,0 +1,23 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-name",
"title": "Product name",
"category": "widgets",
"description": "The product name.",
"keywords": [ "products", "name", "title" ],
"textdomain": "default",
"attributes": {
"name": {
"type": "string"
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
}
}

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createElement } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
import { TextControl } from '@woocommerce/components';
import { useBlockProps } from '@wordpress/block-editor';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityProp } from '@wordpress/core-data';
export function Edit() {
const blockProps = useBlockProps();
const [ name, setName ] = useEntityProp( 'postType', 'product', 'name' );
return (
<div { ...blockProps }>
<TextControl
label={ interpolateComponents( {
mixedString: __( 'Name {{required/}}', 'woocommerce' ),
components: {
required: (
<span className="woocommerce-product-form__optional-input">
{ __( '(required)', 'woocommerce' ) }
</span>
),
},
} ) }
name={ 'woocommerce-product-name' }
placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) }
onChange={ setName }
value={ name || '' }
/>
</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

@ -8,6 +8,11 @@ import {
} from '@wordpress/block-editor';
import { SlotFillProvider } from '@wordpress/components';
import { Product } from '@woocommerce/data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { EntityProvider } from '@wordpress/core-data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
@ -21,35 +26,37 @@ import { FullscreenMode, InterfaceSkeleton } from '@wordpress/interface';
* Internal dependencies
*/
import { Header } from '../header';
import { Sidebar } from '../sidebar';
import { BlockEditor } from '../block-editor';
import { initBlocks } from './init-blocks';
initBlocks();
export type ProductEditorSettings = Partial<
EditorSettings & EditorBlockListSettings
>;
type EditorProps = {
product: Product;
settings: Partial< EditorSettings & EditorBlockListSettings > | undefined;
settings: ProductEditorSettings | undefined;
};
export function Editor( { product, settings }: EditorProps ) {
return (
<StrictMode>
<ShortcutProvider>
<FullscreenMode isActive={ false } />
<SlotFillProvider>
<InterfaceSkeleton
header={ <Header title={ product.name } /> }
sidebar={ <Sidebar /> }
content={
<BlockEditor
settings={ settings }
product={ product }
/>
}
/>
</SlotFillProvider>
</ShortcutProvider>
<EntityProvider kind="postType" type="product" id={ product.id }>
<ShortcutProvider>
<FullscreenMode isActive={ false } />
<SlotFillProvider>
<InterfaceSkeleton
header={ <Header title={ product.name } /> }
content={
<BlockEditor
settings={ settings }
product={ product }
/>
}
/>
</SlotFillProvider>
</ShortcutProvider>
</EntityProvider>
</StrictMode>
);
}

View File

@ -1,5 +1,12 @@
/**
* Internal dependencies
*/
import { init as initName } from '../details-name-block';
import { init as initSection } from '../section';
import { init as initTab } from '../tab';
export const initBlocks = () => {};
export const initBlocks = () => {
initName();
initSection();
initTab();
};

View File

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

View File

@ -0,0 +1,26 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-section",
"title": "Product section",
"category": "woocommerce",
"description": "The product section.",
"keywords": [ "products", "section", "group" ],
"textdomain": "default",
"attributes": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
},
"supports": {
"align": false,
"html": false,
"multiple": true,
"reusable": false,
"inserter": false,
"lock": false
}
}

View File

@ -0,0 +1,23 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import type { BlockAttributes } from '@wordpress/blocks';
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
const blockProps = useBlockProps();
const { description, title } = attributes;
return (
<div { ...blockProps }>
<h2 className="wp-block-woocommerce-product-section__title">
{ title }
</h2>
<p className="wp-block-woocommerce-product-section__description">
{ description }
</p>
<InnerBlocks templateLock="all" />
</div>
);
}

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import initBlock from '../../utils/init-block';
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

@ -0,0 +1,18 @@
.wp-block-woocommerce-product-section {
margin-top: 48px;
&__title {
margin-top: 0;
margin-bottom: $gap-small;
font-size: 24px;
font-weight: 500;
color: $gray-900;
}
&__description {
margin-top: $gap-small;
margin-bottom: 32px;
font-size: 13px;
color: $gray-700;
}
}

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

@ -0,0 +1,27 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-tab",
"title": "Product tab",
"category": "woocommerce",
"description": "The product tab.",
"keywords": [ "products", "tab", "group" ],
"textdomain": "default",
"attributes": {
"id": {
"type": "string"
},
"title": {
"type": "string"
}
},
"supports": {
"align": false,
"html": false,
"multiple": true,
"reusable": false,
"inserter": false,
"lock": false
},
"usesContext": [ "selectedTab" ]
}

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { createElement } from '@wordpress/element';
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import type { BlockAttributes } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { TabButton } from './tab-button';
export function Edit( {
attributes,
context,
}: {
attributes: BlockAttributes;
context?: {
selectedTab?: string | null;
};
} ) {
const blockProps = useBlockProps();
const { id, title } = attributes;
const isSelected = context?.selectedTab === id;
const classes = classnames( 'wp-block-woocommerce-product-tab__content', {
'is-selected': isSelected,
} );
return (
<div { ...blockProps }>
<TabButton id={ id } selected={ isSelected }>
{ title }
</TabButton>
<div
id={ `woocommerce-product-tab__${ id }-content` }
aria-labelledby={ `woocommerce-product-tab__${ id }` }
role="tabpanel"
className={ classes }
>
<InnerBlocks templateLock="all" />
</div>
</div>
);
}

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import initBlock from '../../utils/init-block';
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

@ -0,0 +1,5 @@
.wp-block-woocommerce-product-tab__content {
&:not(.is-selected) {
display: none;
}
}

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { Button, Fill } from '@wordpress/components';
import classnames from 'classnames';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { TABS_SLOT_NAME } from '../tabs/constants';
import { TabsFillProps } from '../tabs';
export function TabButton( {
children,
className,
id,
selected = false,
}: {
children: string | JSX.Element;
className?: string;
id: string;
selected?: boolean;
} ) {
const classes = classnames(
'wp-block-woocommerce-product-tab__button',
className,
{ 'is-selected': selected }
);
return (
<Fill name={ TABS_SLOT_NAME }>
{ ( fillProps: TabsFillProps ) => {
const { onClick } = fillProps;
return (
<Button
key={ id }
className={ classes }
onClick={ () => onClick( id ) }
id={ `woocommerce-product-tab__${ id }` }
aria-controls={ `woocommerce-product-tab__${ id }-content` }
aria-selected={ selected }
>
{ children }
</Button>
);
} }
</Fill>
);
}

View File

@ -0,0 +1 @@
export const TABS_SLOT_NAME = 'woocommerce_product_tabs';

View File

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

View File

@ -0,0 +1,17 @@
.woocommerce-product-tabs {
display: flex;
justify-content: center;
.components-button {
padding: $gap-smaller 0 20px 0;
margin-left: $gap;
margin-right: $gap;
border-bottom: 3.5px solid transparent;
border-radius: 0;
height: auto;
&.is-selected {
border-color: var(--wp-admin-theme-color);
}
}
}

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import {
createElement,
Fragment,
useEffect,
useState,
} from '@wordpress/element';
import { ReactElement } from 'react';
import { NavigableMenu, Slot } from '@wordpress/components';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { TABS_SLOT_NAME } from './constants';
type TabsProps = {
onChange?: ( tabId: string | null ) => void;
};
export type TabsFillProps = {
onClick: ( tabId: string ) => void;
};
export function Tabs( { onChange = () => {} }: TabsProps ) {
const [ selected, setSelected ] = useState< string | null >( null );
const query = getQuery() as Record< string, string >;
function onClick( tabId: string ) {
window.document.documentElement.scrollTop = 0;
navigateTo( {
url: getNewPath( { tab: tabId } ),
} );
}
useEffect( () => {
onChange( selected );
}, [ selected ] );
useEffect( () => {
if ( query.tab ) {
setSelected( query.tab );
}
}, [ query.tab ] );
function maybeSetSelected( fills: readonly ( readonly ReactElement[] )[] ) {
if ( selected ) {
return;
}
for ( let i = 0; i < fills.length; i++ ) {
if ( fills[ i ][ 0 ].props.disabled ) {
continue;
}
// Remove the `.$` prefix on keys. E.g., .$key => key
const tabId = fills[ i ][ 0 ].key?.toString().slice( 2 ) || null;
setSelected( tabId );
return;
}
}
function selectTabOnNavigate(
_childIndex: number,
child: HTMLButtonElement
) {
child.click();
}
return (
<NavigableMenu
role="tablist"
onNavigate={ selectTabOnNavigate }
className="woocommerce-product-tabs"
orientation="horizontal"
>
<Slot
fillProps={
{
onClick,
} as TabsFillProps
}
name={ TABS_SLOT_NAME }
>
{ ( fills ) => {
maybeSetSelected( fills );
return <>{ fills }</>;
} }
</Slot>
</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

@ -5,3 +5,6 @@
@import 'components/details-categories-field/create-category-modal.scss';
@import 'components/header/style.scss';
@import 'components/block-editor/style.scss';
@import 'components/section/style.scss';
@import 'components/tab/style.scss';
@import 'components/tabs/style.scss';

View File

@ -19,6 +19,7 @@ import { preventLeavingProductForm } from './prevent-leaving-product-form';
export * from './create-ordered-children';
export * from './sort-fills-by-order';
export * from './init-blocks';
export {
AUTO_DRAFT_NAME,

View File

@ -0,0 +1,39 @@
/**
* External dependencies
*/
import {
BlockConfiguration,
BlockEditProps,
registerBlockType,
} from '@wordpress/blocks';
import { ComponentType } from 'react';
type BlockRepresentation = {
name: string;
metadata: BlockConfiguration;
settings: Partial< Omit< BlockConfiguration, 'edit' > > & {
readonly edit?:
| ComponentType<
BlockEditProps< object > & {
context?: Record< string, unknown >;
}
>
| undefined;
};
};
/**
* Function to register an individual block.
*
* @param {Object} block The block to be registered.
*
* @return {?WPBlockType} The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export default function initBlock( block: BlockRepresentation ) {
if ( ! block ) {
return;
}
const { metadata, settings, name } = block;
return registerBlockType( { name, ...metadata }, settings );
}

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { BlockConfiguration, registerBlockType } from '@wordpress/blocks';
interface BlockRepresentation {
name: string;
metadata: BlockConfiguration;
settings: Partial< BlockConfiguration >;
}
/**
* Function to register an individual block.
*
* @param {Object} block The block to be registered.
*
* @return {?WPBlockType} The block, if it has been successfully registered;
* otherwise `undefined`.
*/
export const initBlock = ( block: BlockRepresentation ) => {
if ( ! block ) {
return;
}
const { metadata, settings, name } = block;
return registerBlockType( { name, ...metadata }, settings );
};

View File

@ -1,6 +1,11 @@
{
"extends": "../tsconfig-cjs",
"include": [
"**/*",
"src/**/*.json"
],
"compilerOptions": {
"outDir": "build"
"outDir": "build",
"resolveJsonModule": true
}
}

Some files were not shown because too many files have changed in this diff Show More