From 33b2bdc0cd31824a62acdacd724820a9ae66c2f5 Mon Sep 17 00:00:00 2001
From: Florian DANIEL aka Facyla
Date: Fri, 23 Sep 2022 12:47:44 +0200
Subject: [PATCH 01/58] Error message in logs on CSV export error
Provide a useful feedback message when CSV export fails due to wrong permissions on wp-content/upload/ folder (or any other folder set by WC CSV Exporter module).
This helps understand why CSV export fails under some conditions, by providing a hint on the error cause, instead of silently failing.
---
.../includes/export/abstract-wc-csv-batch-exporter.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php
index d65f36bed17..e6f518d269d 100644
--- a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php
+++ b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php
@@ -127,6 +127,7 @@ abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter {
protected function write_csv_data( $data ) {
if ( ! file_exists( $this->get_file_path() ) || ! is_writeable( $this->get_file_path() ) ) {
+ error_log(__("ERROR : Cannot create temporary CSV export file : please check permissions on upload directory.", 'woocommerce'));
return false;
}
From 29c9dfce165ea3a18a67f77e31e080b0e1fe1b37 Mon Sep 17 00:00:00 2001
From: Dion Hulse
Date: Mon, 10 Oct 2022 09:45:50 +1000
Subject: [PATCH 02/58] Get the first array item for the alt_text.
Props galbaras
---
plugins/woocommerce/includes/wc-product-functions.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce/includes/wc-product-functions.php b/plugins/woocommerce/includes/wc-product-functions.php
index 188c8096d44..222ad109eb2 100644
--- a/plugins/woocommerce/includes/wc-product-functions.php
+++ b/plugins/woocommerce/includes/wc-product-functions.php
@@ -819,7 +819,7 @@ function wc_get_product_attachment_props( $attachment_id = null, $product = fals
}
$alt_text = array_filter( $alt_text );
- $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : '';
+ $props['alt'] = $alt_text ? reset( $alt_text ) : '';
// Large version.
$full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) );
From 2a034c0df4108e7e954a71d3a8917b2b54f79485 Mon Sep 17 00:00:00 2001
From: Phill <38789408+SavPhill@users.noreply.github.com>
Date: Sat, 15 Oct 2022 15:43:52 +0700
Subject: [PATCH 03/58] Update tooltip text
The tooltip for the Header Image field currently reads: "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media)."
I believe my small edit to the description text makes it more clearer for how the user should do this.
---
.../includes/admin/settings/class-wc-settings-emails.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php
index 9086d7fd8b8..36705f13c50 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-emails.php
@@ -120,7 +120,7 @@ class WC_Settings_Emails extends WC_Settings_Page {
array(
'title' => __( 'Header image', 'woocommerce' ),
- 'desc' => __( 'URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).', 'woocommerce' ),
+ 'desc' => __( 'Paste the URL of an image you want to show in the email header. Upload images using the media uploader (Media > Add New).', 'woocommerce' ),
'id' => 'woocommerce_email_header_image',
'type' => 'text',
'css' => 'min-width:400px;',
From 1b5bc44c60810413d01e1430c7c8dca4fb990375 Mon Sep 17 00:00:00 2001
From: Vedanshu Jain
Date: Wed, 2 Nov 2022 20:14:55 +0530
Subject: [PATCH 04/58] Add changelog.
---
plugins/woocommerce/changelog/pr-35107 | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 plugins/woocommerce/changelog/pr-35107
diff --git a/plugins/woocommerce/changelog/pr-35107 b/plugins/woocommerce/changelog/pr-35107
new file mode 100644
index 00000000000..4a03f11bfc9
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-35107
@@ -0,0 +1,5 @@
+Significance: patch
+Type: fix
+Comment: Only a minor text change.
+
+
From 4647efa84066d353cd2829e3cb9d8e0925a20cf8 Mon Sep 17 00:00:00 2001
From: Timur Gogolev
Date: Mon, 26 Dec 2022 16:02:23 +0300
Subject: [PATCH 05/58] Include WC Cart functions for REST API calls
---
plugins/woocommerce/changelog/fix-missed-wc-empty-cart | 4 ++++
plugins/woocommerce/includes/class-wc-session-handler.php | 6 +++++-
2 files changed, 9 insertions(+), 1 deletion(-)
create mode 100644 plugins/woocommerce/changelog/fix-missed-wc-empty-cart
diff --git a/plugins/woocommerce/changelog/fix-missed-wc-empty-cart b/plugins/woocommerce/changelog/fix-missed-wc-empty-cart
new file mode 100644
index 00000000000..cb2d0b4ecb1
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-missed-wc-empty-cart
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Load wc_empty_cart function for REST API calls.
diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php
index 332ecb5e7f6..02cda0a3b0c 100644
--- a/plugins/woocommerce/includes/class-wc-session-handler.php
+++ b/plugins/woocommerce/includes/class-wc-session-handler.php
@@ -369,7 +369,11 @@ class WC_Session_Handler extends WC_Session {
public function forget_session() {
wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true );
- wc_empty_cart();
+ if( ! is_admin() ) {
+ include_once WC_ABSPATH . 'includes/wc-cart-functions.php';
+
+ wc_empty_cart();
+ }
$this->_data = array();
$this->_dirty = false;
From abd4c8da80909e9bb424d17ff2643c00f14f2657 Mon Sep 17 00:00:00 2001
From: Timur Gogolev
Date: Mon, 26 Dec 2022 16:34:20 +0300
Subject: [PATCH 06/58] Fix an issue found by PHPCS
---
plugins/woocommerce/includes/class-wc-session-handler.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce/includes/class-wc-session-handler.php b/plugins/woocommerce/includes/class-wc-session-handler.php
index 02cda0a3b0c..046583dfe45 100644
--- a/plugins/woocommerce/includes/class-wc-session-handler.php
+++ b/plugins/woocommerce/includes/class-wc-session-handler.php
@@ -369,7 +369,7 @@ class WC_Session_Handler extends WC_Session {
public function forget_session() {
wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true );
- if( ! is_admin() ) {
+ if ( ! is_admin() ) {
include_once WC_ABSPATH . 'includes/wc-cart-functions.php';
wc_empty_cart();
From dc2a0ff774e36cfd2b5c4991e354b3e6b01d62e8 Mon Sep 17 00:00:00 2001
From: barryhughes <3594411+barryhughes@users.noreply.github.com>
Date: Thu, 12 Jan 2023 16:26:54 -0800
Subject: [PATCH 07/58] Changelog.
---
plugins/woocommerce/changelog/34071-alt-text-handling | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 plugins/woocommerce/changelog/34071-alt-text-handling
diff --git a/plugins/woocommerce/changelog/34071-alt-text-handling b/plugins/woocommerce/changelog/34071-alt-text-handling
new file mode 100644
index 00000000000..fea6c96482b
--- /dev/null
+++ b/plugins/woocommerce/changelog/34071-alt-text-handling
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Improve the way we retrieve the alt text property for product attachments.
From 2dce575449cb6710ffe0a86561f78d39bb2080c7 Mon Sep 17 00:00:00 2001
From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com>
Date: Mon, 16 Jan 2023 04:13:55 -0800
Subject: [PATCH 08/58] Adding ProductForm data store (#36430)
* Removing overriding functions in Section.php
* Adding changelog
* Adding product form data store
* Adding data package changelog
---
.../add-36073-product-form-data-store | 4 ++
packages/js/data/src/index.ts | 6 +-
.../js/data/src/product-form/action-types.ts | 8 +++
packages/js/data/src/product-form/actions.ts | 42 +++++++++++++
.../js/data/src/product-form/constants.ts | 1 +
packages/js/data/src/product-form/index.ts | 37 +++++++++++
packages/js/data/src/product-form/reducer.ts | 63 +++++++++++++++++++
.../js/data/src/product-form/resolvers.ts | 53 ++++++++++++++++
.../js/data/src/product-form/selectors.ts | 17 +++++
packages/js/data/src/product-form/types.ts | 35 +++++++++++
.../add-36073-product-form-data-store | 4 ++
.../Internal/Admin/ProductForm/Section.php | 22 -------
12 files changed, 269 insertions(+), 23 deletions(-)
create mode 100644 packages/js/data/changelog/add-36073-product-form-data-store
create mode 100644 packages/js/data/src/product-form/action-types.ts
create mode 100644 packages/js/data/src/product-form/actions.ts
create mode 100644 packages/js/data/src/product-form/constants.ts
create mode 100644 packages/js/data/src/product-form/index.ts
create mode 100644 packages/js/data/src/product-form/reducer.ts
create mode 100644 packages/js/data/src/product-form/resolvers.ts
create mode 100644 packages/js/data/src/product-form/selectors.ts
create mode 100644 packages/js/data/src/product-form/types.ts
create mode 100644 plugins/woocommerce/changelog/add-36073-product-form-data-store
diff --git a/packages/js/data/changelog/add-36073-product-form-data-store b/packages/js/data/changelog/add-36073-product-form-data-store
new file mode 100644
index 00000000000..801045b961d
--- /dev/null
+++ b/packages/js/data/changelog/add-36073-product-form-data-store
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding Product Form data store.
diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts
index 51c0d88a515..c0af7842a32 100644
--- a/packages/js/data/src/index.ts
+++ b/packages/js/data/src/index.ts
@@ -25,6 +25,7 @@ export { EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME } from './product-tags';
export { EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME } from './product-categories';
export { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME } from './product-attribute-terms';
export { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME } from './product-variations';
+export { EXPERIMENTAL_PRODUCT_FORM_STORE_NAME } from './product-form';
export { EXPERIMENTAL_TAX_CLASSES_STORE_NAME } from './tax-classes';
export { PaymentGateway } from './payment-gateways/types';
@@ -75,6 +76,7 @@ export {
// Export types
export * from './types';
export * from './countries/types';
+export { ProductForm } from './product-form/types';
export * from './onboarding/types';
export * from './plugins/types';
export * from './products/types';
@@ -122,6 +124,7 @@ import type { EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME } from './product
import type { EXPERIMENTAL_SHIPPING_ZONES_STORE_NAME } from './shipping-zones';
import type { EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME } from './product-tags';
import type { EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME } from './product-categories';
+import type { EXPERIMENTAL_PRODUCT_FORM_STORE_NAME } from './product-form';
import type { EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME } from './product-attribute-terms';
import type { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME } from './product-variations';
import type { EXPERIMENTAL_TAX_CLASSES_STORE_NAME } from './tax-classes';
@@ -148,7 +151,8 @@ export type WCDataStoreName =
| typeof EXPERIMENTAL_PRODUCT_TAGS_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_CATEGORIES_STORE_NAME
| typeof EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
- | typeof EXPERIMENTAL_TAX_CLASSES_STORE_NAME;
+ | typeof EXPERIMENTAL_TAX_CLASSES_STORE_NAME
+ | typeof EXPERIMENTAL_PRODUCT_FORM_STORE_NAME;
/**
* Internal dependencies
diff --git a/packages/js/data/src/product-form/action-types.ts b/packages/js/data/src/product-form/action-types.ts
new file mode 100644
index 00000000000..823b1e267f5
--- /dev/null
+++ b/packages/js/data/src/product-form/action-types.ts
@@ -0,0 +1,8 @@
+export enum TYPES {
+ GET_FIELDS_ERROR = 'GET_FIELDS_ERROR',
+ GET_FIELDS_SUCCESS = 'GET_FIELDS_SUCCESS',
+ GET_PRODUCT_FORM_ERROR = 'GET_PRODUCT_FORM_ERROR',
+ GET_PRODUCT_FORM_SUCCESS = 'GET_PRODUCT_FORM_SUCCESS',
+}
+
+export default TYPES;
diff --git a/packages/js/data/src/product-form/actions.ts b/packages/js/data/src/product-form/actions.ts
new file mode 100644
index 00000000000..e5e2288a838
--- /dev/null
+++ b/packages/js/data/src/product-form/actions.ts
@@ -0,0 +1,42 @@
+/**
+ * Internal dependencies
+ */
+import TYPES from './action-types';
+import { Field, ProductForm } from './types';
+
+export function getFieldsSuccess( fields: Field[] ) {
+ return {
+ type: TYPES.GET_FIELDS_SUCCESS as const,
+ fields,
+ };
+}
+
+export function getFieldsError( error: unknown ) {
+ return {
+ type: TYPES.GET_FIELDS_ERROR as const,
+ error,
+ };
+}
+
+export function getProductFormSuccess( productForm: ProductForm ) {
+ return {
+ type: TYPES.GET_PRODUCT_FORM_SUCCESS as const,
+ fields: productForm.fields,
+ sections: productForm.sections,
+ subsections: productForm.subsections,
+ };
+}
+
+export function getProductFormError( error: unknown ) {
+ return {
+ type: TYPES.GET_PRODUCT_FORM_ERROR as const,
+ error,
+ };
+}
+
+export type Action = ReturnType<
+ | typeof getFieldsSuccess
+ | typeof getFieldsError
+ | typeof getProductFormSuccess
+ | typeof getProductFormError
+>;
diff --git a/packages/js/data/src/product-form/constants.ts b/packages/js/data/src/product-form/constants.ts
new file mode 100644
index 00000000000..d295446042e
--- /dev/null
+++ b/packages/js/data/src/product-form/constants.ts
@@ -0,0 +1 @@
+export const STORE_NAME = 'experimental/wc/admin/product-form';
diff --git a/packages/js/data/src/product-form/index.ts b/packages/js/data/src/product-form/index.ts
new file mode 100644
index 00000000000..0a6dfbef68d
--- /dev/null
+++ b/packages/js/data/src/product-form/index.ts
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import { registerStore } from '@wordpress/data';
+import { controls } from '@wordpress/data-controls';
+import { SelectFromMap, DispatchFromMap } from '@automattic/data-stores';
+import { Reducer, AnyAction } from 'redux';
+/**
+ * Internal dependencies
+ */
+import { STORE_NAME } from './constants';
+import * as selectors from './selectors';
+import * as actions from './actions';
+import * as resolvers from './resolvers';
+import reducer, { State } from './reducer';
+import { WPDataSelectors } from '../types';
+export * from './types';
+export type { State };
+
+registerStore< State >( STORE_NAME, {
+ reducer: reducer as Reducer< State, AnyAction >,
+ actions,
+ controls,
+ selectors,
+ resolvers,
+} );
+
+export const EXPERIMENTAL_PRODUCT_FORM_STORE_NAME = STORE_NAME;
+
+declare module '@wordpress/data' {
+ function dispatch(
+ key: typeof STORE_NAME
+ ): DispatchFromMap< typeof actions >;
+ function select(
+ key: typeof STORE_NAME
+ ): SelectFromMap< typeof selectors > & WPDataSelectors;
+}
diff --git a/packages/js/data/src/product-form/reducer.ts b/packages/js/data/src/product-form/reducer.ts
new file mode 100644
index 00000000000..6757619bd1a
--- /dev/null
+++ b/packages/js/data/src/product-form/reducer.ts
@@ -0,0 +1,63 @@
+/**
+ * External dependencies
+ */
+
+import type { Reducer } from 'redux';
+
+/**
+ * Internal dependencies
+ */
+import TYPES from './action-types';
+import { Action } from './actions';
+import { ProductFormState } from './types';
+
+const reducer: Reducer< ProductFormState, Action > = (
+ state = {
+ errors: {},
+ fields: [],
+ sections: [],
+ subsections: [],
+ },
+ action
+) => {
+ switch ( action.type ) {
+ case TYPES.GET_FIELDS_SUCCESS:
+ state = {
+ ...state,
+ fields: action.fields,
+ };
+ break;
+ case TYPES.GET_FIELDS_ERROR:
+ state = {
+ ...state,
+ errors: {
+ ...state.errors,
+ fields: action.error,
+ },
+ };
+ break;
+ case TYPES.GET_PRODUCT_FORM_SUCCESS:
+ state = {
+ ...state,
+ fields: action.fields,
+ sections: action.sections,
+ subsections: action.subsections,
+ };
+ break;
+ case TYPES.GET_PRODUCT_FORM_ERROR:
+ state = {
+ ...state,
+ errors: {
+ ...state.errors,
+ fields: action.error,
+ sections: action.error,
+ subsections: action.error,
+ },
+ };
+ break;
+ }
+ return state;
+};
+
+export type State = ReturnType< typeof reducer >;
+export default reducer;
diff --git a/packages/js/data/src/product-form/resolvers.ts b/packages/js/data/src/product-form/resolvers.ts
new file mode 100644
index 00000000000..632a2dc5a57
--- /dev/null
+++ b/packages/js/data/src/product-form/resolvers.ts
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { apiFetch, select } from '@wordpress/data-controls';
+import { controls } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import {
+ getFieldsSuccess,
+ getFieldsError,
+ getProductFormSuccess,
+ getProductFormError,
+} from './actions';
+import { WC_ADMIN_NAMESPACE } from '../constants';
+import { Field, ProductForm } from './types';
+import { STORE_NAME } from './constants';
+
+const resolveSelect =
+ controls && controls.resolveSelect ? controls.resolveSelect : select;
+
+export function* getFields() {
+ try {
+ const url = WC_ADMIN_NAMESPACE + '/product-form/fields';
+ const results: Field[] = yield apiFetch( {
+ path: url,
+ method: 'GET',
+ } );
+
+ return getFieldsSuccess( results );
+ } catch ( error ) {
+ return getFieldsError( error );
+ }
+}
+
+export function* getCountry() {
+ yield resolveSelect( STORE_NAME, 'getProductForm' );
+}
+
+export function* getProductForm() {
+ try {
+ const url = WC_ADMIN_NAMESPACE + '/product-form';
+ const results: ProductForm = yield apiFetch( {
+ path: url,
+ method: 'GET',
+ } );
+
+ return getProductFormSuccess( results );
+ } catch ( error ) {
+ return getProductFormError( error );
+ }
+}
diff --git a/packages/js/data/src/product-form/selectors.ts b/packages/js/data/src/product-form/selectors.ts
new file mode 100644
index 00000000000..d986b006cc7
--- /dev/null
+++ b/packages/js/data/src/product-form/selectors.ts
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import { ProductFormState } from './types';
+
+export const getFields = ( state: ProductFormState ) => {
+ return state.fields;
+};
+
+export const getField = ( state: ProductFormState, id: string ) => {
+ return state.fields.find( ( field ) => field.id === id );
+};
+
+export const getProductForm = ( state: ProductFormState ) => {
+ const { errors, ...form } = state;
+ return form;
+};
diff --git a/packages/js/data/src/product-form/types.ts b/packages/js/data/src/product-form/types.ts
new file mode 100644
index 00000000000..d4dcc8319b1
--- /dev/null
+++ b/packages/js/data/src/product-form/types.ts
@@ -0,0 +1,35 @@
+type BaseComponent = {
+ id: string;
+ plugin_id: string;
+ order: number;
+};
+
+type FieldProperties = {
+ name: string;
+ label: string;
+};
+
+export type Field = BaseComponent & {
+ type: string;
+ section: string;
+ properties: FieldProperties;
+};
+
+export type Section = BaseComponent & {
+ title: string;
+ description: string;
+};
+
+export type Subsection = BaseComponent;
+
+export type ProductForm = {
+ fields: Field[];
+ sections: Section[];
+ subsections: Subsection[];
+};
+
+export type ProductFormState = ProductForm & {
+ errors: {
+ [ key: string ]: unknown;
+ };
+};
diff --git a/plugins/woocommerce/changelog/add-36073-product-form-data-store b/plugins/woocommerce/changelog/add-36073-product-form-data-store
new file mode 100644
index 00000000000..bb0f8989152
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-36073-product-form-data-store
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding JS data store for ProductForm.
diff --git a/plugins/woocommerce/src/Internal/Admin/ProductForm/Section.php b/plugins/woocommerce/src/Internal/Admin/ProductForm/Section.php
index 049be3f4ef4..b86512bdb7a 100644
--- a/plugins/woocommerce/src/Internal/Admin/ProductForm/Section.php
+++ b/plugins/woocommerce/src/Internal/Admin/ProductForm/Section.php
@@ -54,28 +54,6 @@ class Section extends Component {
$this->title = $additional_args['title'];
}
- /**
- * Field arguments.
- *
- * @return array
- */
- public function get_arguments() {
- return $this->additional_args;
- }
-
- /**
- * Get the section as JSON.
- *
- * @return array
- */
- public function get_json() {
- return array(
- 'id' => $this->get_id(),
- 'plugin_id' => $this->get_plugin_id(),
- 'arguments' => $this->get_arguments(),
- );
- }
-
/**
* Get missing arguments of args array.
*
From d935b0137ab84dc2cffb0c59f659de50f6fe16e5 Mon Sep 17 00:00:00 2001
From: Roy Ho
Date: Mon, 16 Jan 2023 06:12:25 -0800
Subject: [PATCH 09/58] Add necessary permissions for code freeze workflows
(#36399)
* Add necessary permissions for code freeze workflows
* Checkout pnpm-lock.yaml to prevent issues
---
.github/workflows/release-changelog.yml | 7 +++++--
.github/workflows/release-code-freeze.yml | 9 +++++----
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/release-changelog.yml b/.github/workflows/release-changelog.yml
index b8ce8d8572a..ab77d208efb 100644
--- a/.github/workflows/release-changelog.yml
+++ b/.github/workflows/release-changelog.yml
@@ -21,8 +21,8 @@ jobs:
create-changelog-prs:
runs-on: ubuntu-20.04
permissions:
- contents: read
- pull-requests: write
+ contents: write
+ pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v3
@@ -46,6 +46,9 @@ jobs:
- name: 'Generate the changelog file'
run: pnpm --filter=woocommerce run changelog write --add-pr-num -n -vvv --use-version ${{ inputs.releaseVersion }}
+ - name: Checkout pnpm-lock.yaml to prevent issues
+ run: git checkout pnpm-lock.yaml
+
- name: 'git rm deleted files'
run: git rm $(git ls-files --deleted)
diff --git a/.github/workflows/release-code-freeze.yml b/.github/workflows/release-code-freeze.yml
index ffb31eea89c..1a73fddb62b 100644
--- a/.github/workflows/release-code-freeze.yml
+++ b/.github/workflows/release-code-freeze.yml
@@ -60,7 +60,8 @@ jobs:
name: 'Maybe create next milestone and release branch'
runs-on: ubuntu-20.04
permissions:
- contents: read
+ contents: write
+ issues: write
needs: verify-code-freeze
if: needs.verify-code-freeze.outputs.freeze == 0
outputs:
@@ -89,8 +90,8 @@ jobs:
name: Preps trunk for next development cycle
runs-on: ubuntu-20.04
permissions:
- contents: read
- pull-requests: write
+ contents: write
+ pull-requests: write
needs: maybe-create-next-milestone-and-release-branch
steps:
- name: Checkout code
@@ -159,7 +160,7 @@ jobs:
name: 'Trigger changelog action'
runs-on: ubuntu-20.04
permissions:
- actions: write
+ actions: write
needs: maybe-create-next-milestone-and-release-branch
steps:
- name: 'Trigger changelog action'
From 0d6af37a66b8b3a42a96dc654791d84315741321 Mon Sep 17 00:00:00 2001
From: Joshua T Flowers
Date: Mon, 16 Jan 2023 07:54:33 -0800
Subject: [PATCH 10/58] Add requesting state, actions, and selectors to CRUD
data stores (#36297)
* Add requesting state, actions, and selectors to CRUD data stores
* Allow resource specific selectors for requesting status
* Use constants for action names in reducer
* Add tests around new utils
* Add changelog entry
* Use getResourceName instead of getRequestKey in reducer
* Rename and use getRequestIdentifier to avoid ambiguity with resource names
* Sanitize and replace ID query with key in resolution selectors
* Fix up crud action names and remove camel casing
* Fix missing ID in ID query check
* Fix up tests around request identifier
* Add additional tests around utils
* Clean up imports
* Add missing selectors to tests
---
packages/js/data/changelog/add-36189 | 4 +
packages/js/data/src/crud/action-types.ts | 3 +
packages/js/data/src/crud/actions.ts | 61 +++++++-
packages/js/data/src/crud/reducer.ts | 162 ++++++++++++++++++--
packages/js/data/src/crud/selectors.ts | 62 ++++++--
packages/js/data/src/crud/test/reducer.ts | 79 ++++++++--
packages/js/data/src/crud/test/selectors.ts | 4 +-
packages/js/data/src/crud/test/utils.ts | 105 +++++++++++++
packages/js/data/src/crud/utils.ts | 90 +++++++++++
9 files changed, 524 insertions(+), 46 deletions(-)
create mode 100644 packages/js/data/changelog/add-36189
diff --git a/packages/js/data/changelog/add-36189 b/packages/js/data/changelog/add-36189
new file mode 100644
index 00000000000..581419992cd
--- /dev/null
+++ b/packages/js/data/changelog/add-36189
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add ability to check CRUD dispatch action status
diff --git a/packages/js/data/src/crud/action-types.ts b/packages/js/data/src/crud/action-types.ts
index fac68896739..903d63535c0 100644
--- a/packages/js/data/src/crud/action-types.ts
+++ b/packages/js/data/src/crud/action-types.ts
@@ -1,13 +1,16 @@
export enum TYPES {
CREATE_ITEM_ERROR = 'CREATE_ITEM_ERROR',
+ CREATE_ITEM_REQUEST = 'CREATE_ITEM_REQUEST',
CREATE_ITEM_SUCCESS = 'CREATE_ITEM_SUCCESS',
DELETE_ITEM_ERROR = 'DELETE_ITEM_ERROR',
+ DELETE_ITEM_REQUEST = 'DELETE_ITEM_REQUEST',
DELETE_ITEM_SUCCESS = 'DELETE_ITEM_SUCCESS',
GET_ITEM_ERROR = 'GET_ITEM_ERROR',
GET_ITEM_SUCCESS = 'GET_ITEM_SUCCESS',
GET_ITEMS_ERROR = 'GET_ITEMS_ERROR',
GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS',
UPDATE_ITEM_ERROR = 'UPDATE_ITEM_ERROR',
+ UPDATE_ITEM_REQUEST = 'UPDATE_ITEM_REQUEST',
UPDATE_ITEM_SUCCESS = 'UPDATE_ITEM_SUCCESS',
GET_ITEMS_TOTAL_COUNT_SUCCESS = 'GET_ITEMS_TOTAL_COUNT_SUCCESS',
GET_ITEMS_TOTAL_COUNT_ERROR = 'GET_ITEMS_TOTAL_COUNT_ERROR',
diff --git a/packages/js/data/src/crud/actions.ts b/packages/js/data/src/crud/actions.ts
index 2743c329505..f708955f6a6 100644
--- a/packages/js/data/src/crud/actions.ts
+++ b/packages/js/data/src/crud/actions.ts
@@ -25,20 +25,41 @@ export function createItemError( query: Partial< ItemQuery >, error: unknown ) {
};
}
-export function createItemSuccess( key: IdType, item: Item ) {
+export function createItemRequest( query: Partial< ItemQuery > ) {
+ return {
+ type: TYPES.CREATE_ITEM_REQUEST as const,
+ query,
+ };
+}
+
+export function createItemSuccess(
+ key: IdType,
+ item: Item,
+ query: Partial< ItemQuery >
+) {
return {
type: TYPES.CREATE_ITEM_SUCCESS as const,
key,
item,
+ query,
};
}
-export function deleteItemError( key: IdType, error: unknown ) {
+export function deleteItemError( key: IdType, error: unknown, force: boolean ) {
return {
type: TYPES.DELETE_ITEM_ERROR as const,
key,
error,
errorType: CRUD_ACTIONS.DELETE_ITEM,
+ force,
+ };
+}
+
+export function deleteItemRequest( key: IdType, force: boolean ) {
+ return {
+ type: TYPES.DELETE_ITEM_REQUEST as const,
+ key,
+ force,
};
}
@@ -116,20 +137,38 @@ export function getItemsTotalCountError(
};
}
-export function updateItemError( key: IdType, error: unknown ) {
+export function updateItemError(
+ key: IdType,
+ error: unknown,
+ query: Partial< ItemQuery >
+) {
return {
type: TYPES.UPDATE_ITEM_ERROR as const,
key,
error,
errorType: CRUD_ACTIONS.UPDATE_ITEM,
+ query,
};
}
-export function updateItemSuccess( key: IdType, item: Item ) {
+export function updateItemRequest( key: IdType, query: Partial< ItemQuery > ) {
+ return {
+ type: TYPES.UPDATE_ITEM_REQUEST as const,
+ key,
+ query,
+ };
+}
+
+export function updateItemSuccess(
+ key: IdType,
+ item: Item,
+ query: Partial< ItemQuery >
+) {
return {
type: TYPES.UPDATE_ITEM_SUCCESS as const,
key,
item,
+ query,
};
}
@@ -138,6 +177,7 @@ export const createDispatchActions = ( {
resourceName,
}: ResolverOptions ) => {
const createItem = function* ( query: Partial< ItemQuery > ) {
+ yield createItemRequest( query );
const urlParameters = getUrlParameters( namespace, query );
try {
@@ -151,7 +191,7 @@ export const createDispatchActions = ( {
} );
const { key } = parseId( item.id, urlParameters );
- yield createItemSuccess( key, item );
+ yield createItemSuccess( key, item, query );
return item;
} catch ( error ) {
yield createItemError( query, error );
@@ -162,6 +202,7 @@ export const createDispatchActions = ( {
const deleteItem = function* ( idQuery: IdQuery, force = true ) {
const urlParameters = getUrlParameters( namespace, idQuery );
const { id, key } = parseId( idQuery, urlParameters );
+ yield deleteItemRequest( key, force );
try {
const item: Item = yield apiFetch( {
@@ -176,7 +217,7 @@ export const createDispatchActions = ( {
yield deleteItemSuccess( key, force, item );
return item;
} catch ( error ) {
- yield deleteItemError( key, error );
+ yield deleteItemError( key, error, force );
throw error;
}
};
@@ -187,6 +228,7 @@ export const createDispatchActions = ( {
) {
const urlParameters = getUrlParameters( namespace, idQuery );
const { id, key } = parseId( idQuery, urlParameters );
+ yield updateItemRequest( key, query );
try {
const item: Item = yield apiFetch( {
@@ -199,10 +241,10 @@ export const createDispatchActions = ( {
data: query,
} );
- yield updateItemSuccess( key, item );
+ yield updateItemSuccess( key, item, query );
return item;
} catch ( error ) {
- yield updateItemError( key, error );
+ yield updateItemError( key, error, query );
throw error;
}
};
@@ -216,8 +258,10 @@ export const createDispatchActions = ( {
export type Actions = ReturnType<
| typeof createItemError
+ | typeof createItemRequest
| typeof createItemSuccess
| typeof deleteItemError
+ | typeof deleteItemRequest
| typeof deleteItemSuccess
| typeof getItemError
| typeof getItemSuccess
@@ -226,5 +270,6 @@ export type Actions = ReturnType<
| typeof getItemsTotalCountSuccess
| typeof getItemsTotalCountError
| typeof updateItemError
+ | typeof updateItemRequest
| typeof updateItemSuccess
>;
diff --git a/packages/js/data/src/crud/reducer.ts b/packages/js/data/src/crud/reducer.ts
index 87b12520c98..baa2de63c1f 100644
--- a/packages/js/data/src/crud/reducer.ts
+++ b/packages/js/data/src/crud/reducer.ts
@@ -8,8 +8,8 @@ import { Reducer } from 'redux';
*/
import { Actions } from './actions';
import CRUD_ACTIONS from './crud-actions';
-import { getKey } from './utils';
-import { getResourceName, getTotalCountResourceName } from '../utils';
+import { getKey, getRequestIdentifier } from './utils';
+import { getTotalCountResourceName } from '../utils';
import { IdType, Item, ItemQuery } from './types';
import { TYPES } from './action-types';
@@ -24,6 +24,7 @@ export type ResourceState = {
data: Data;
itemsCount: Record< string, number >;
errors: Record< string, unknown >;
+ requesting: Record< string, boolean >;
};
export const createReducer = () => {
@@ -33,19 +34,37 @@ export const createReducer = () => {
data: {},
itemsCount: {},
errors: {},
+ requesting: {},
},
payload
) => {
+ const itemData = state.data || {};
+
if ( payload && 'type' in payload ) {
switch ( payload.type ) {
case TYPES.CREATE_ITEM_ERROR:
+ const createItemErrorRequestId = getRequestIdentifier(
+ payload.errorType,
+ payload.query || {}
+ );
+ return {
+ ...state,
+ errors: {
+ ...state.errors,
+ [ createItemErrorRequestId ]: payload.error,
+ },
+ requesting: {
+ ...state.requesting,
+ [ createItemErrorRequestId ]: false,
+ },
+ };
case TYPES.GET_ITEMS_TOTAL_COUNT_ERROR:
case TYPES.GET_ITEMS_ERROR:
return {
...state,
errors: {
...state.errors,
- [ getResourceName(
+ [ getRequestIdentifier(
payload.errorType,
( payload.query || {} ) as ItemQuery
) ]: payload.error,
@@ -64,9 +83,27 @@ export const createReducer = () => {
};
case TYPES.CREATE_ITEM_SUCCESS:
+ const createItemSuccessRequestId = getRequestIdentifier(
+ CRUD_ACTIONS.CREATE_ITEM,
+ payload.key,
+ payload.query
+ );
+ return {
+ ...state,
+ data: {
+ ...itemData,
+ [ payload.key ]: {
+ ...( itemData[ payload.key ] || {} ),
+ ...payload.item,
+ },
+ },
+ requesting: {
+ ...state.requesting,
+ [ createItemSuccessRequestId ]: false,
+ },
+ };
+
case TYPES.GET_ITEM_SUCCESS:
- case TYPES.UPDATE_ITEM_SUCCESS:
- const itemData = state.data || {};
return {
...state,
data: {
@@ -78,7 +115,33 @@ export const createReducer = () => {
},
};
+ case TYPES.UPDATE_ITEM_SUCCESS:
+ const updateItemSuccessRequestId = getRequestIdentifier(
+ CRUD_ACTIONS.UPDATE_ITEM,
+ payload.key,
+ payload.query
+ );
+ return {
+ ...state,
+ data: {
+ ...itemData,
+ [ payload.key ]: {
+ ...( itemData[ payload.key ] || {} ),
+ ...payload.item,
+ },
+ },
+ requesting: {
+ ...state.requesting,
+ [ updateItemSuccessRequestId ]: false,
+ },
+ };
+
case TYPES.DELETE_ITEM_SUCCESS:
+ const deleteItemSuccessRequestId = getRequestIdentifier(
+ CRUD_ACTIONS.DELETE_ITEM,
+ payload.key,
+ payload.force
+ );
const itemKeys = Object.keys( state.data );
const nextData = itemKeys.reduce< Data >(
( items: Data, key: string ) => {
@@ -98,18 +161,57 @@ export const createReducer = () => {
return {
...state,
data: nextData,
+ requesting: {
+ ...state.requesting,
+ [ deleteItemSuccessRequestId ]: false,
+ },
};
case TYPES.DELETE_ITEM_ERROR:
- case TYPES.GET_ITEM_ERROR:
- case TYPES.UPDATE_ITEM_ERROR:
+ const deleteItemErrorRequestId = getRequestIdentifier(
+ payload.errorType,
+ payload.key,
+ payload.force
+ );
return {
...state,
errors: {
...state.errors,
- [ getResourceName( payload.errorType, {
- key: payload.key,
- } ) ]: payload.error,
+ [ deleteItemErrorRequestId ]: payload.error,
+ },
+ requesting: {
+ ...state.requesting,
+ [ deleteItemErrorRequestId ]: false,
+ },
+ };
+
+ case TYPES.GET_ITEM_ERROR:
+ return {
+ ...state,
+ errors: {
+ ...state.errors,
+ [ getRequestIdentifier(
+ payload.errorType,
+ payload.key
+ ) ]: payload.error,
+ },
+ };
+
+ case TYPES.UPDATE_ITEM_ERROR:
+ const upateItemErrorRequestId = getRequestIdentifier(
+ payload.errorType,
+ payload.key,
+ payload.query
+ );
+ return {
+ ...state,
+ errors: {
+ ...state.errors,
+ [ upateItemErrorRequestId ]: payload.error,
+ },
+ requesting: {
+ ...state.requesting,
+ [ upateItemErrorRequestId ]: false,
},
};
@@ -128,7 +230,7 @@ export const createReducer = () => {
return result;
}, {} );
- const itemQuery = getResourceName(
+ const itemQuery = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS,
( payload.query || {} ) as ItemQuery
);
@@ -145,6 +247,44 @@ export const createReducer = () => {
},
};
+ case TYPES.CREATE_ITEM_REQUEST:
+ return {
+ ...state,
+ requesting: {
+ ...state.requesting,
+ [ getRequestIdentifier(
+ CRUD_ACTIONS.CREATE_ITEM,
+ payload.query
+ ) ]: true,
+ },
+ };
+
+ case TYPES.DELETE_ITEM_REQUEST:
+ return {
+ ...state,
+ requesting: {
+ ...state.requesting,
+ [ getRequestIdentifier(
+ CRUD_ACTIONS.DELETE_ITEM,
+ payload.key,
+ payload.force
+ ) ]: true,
+ },
+ };
+
+ case TYPES.UPDATE_ITEM_REQUEST:
+ return {
+ ...state,
+ requesting: {
+ ...state.requesting,
+ [ getRequestIdentifier(
+ CRUD_ACTIONS.UPDATE_ITEM,
+ payload.key,
+ payload.query
+ ) ]: true,
+ },
+ };
+
default:
return state;
}
diff --git a/packages/js/data/src/crud/selectors.ts b/packages/js/data/src/crud/selectors.ts
index 05d019a7b04..f95d0e31bc3 100644
--- a/packages/js/data/src/crud/selectors.ts
+++ b/packages/js/data/src/crud/selectors.ts
@@ -6,8 +6,15 @@ import createSelector from 'rememo';
/**
* Internal dependencies
*/
-import { applyNamespace, getUrlParameters, parseId } from './utils';
-import { getResourceName, getTotalCountResourceName } from '../utils';
+import {
+ applyNamespace,
+ getGenericActionName,
+ getRequestIdentifier,
+ getUrlParameters,
+ maybeReplaceIdQuery,
+ parseId,
+} from './utils';
+import { getTotalCountResourceName } from '../utils';
import { IdQuery, IdType, Item, ItemQuery } from './types';
import { ResourceState } from './reducer';
import CRUD_ACTIONS from './crud-actions';
@@ -22,7 +29,7 @@ export const getItemCreateError = (
state: ResourceState,
query: ItemQuery
) => {
- const itemQuery = getResourceName( CRUD_ACTIONS.CREATE_ITEM, query );
+ const itemQuery = getRequestIdentifier( CRUD_ACTIONS.CREATE_ITEM, query );
return state.errors[ itemQuery ];
};
@@ -33,7 +40,7 @@ export const getItemDeleteError = (
) => {
const urlParameters = getUrlParameters( namespace, idQuery );
const { key } = parseId( idQuery, urlParameters );
- const itemQuery = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { key } );
+ const itemQuery = getRequestIdentifier( CRUD_ACTIONS.DELETE_ITEM, key );
return state.errors[ itemQuery ];
};
@@ -54,13 +61,13 @@ export const getItemError = (
) => {
const urlParameters = getUrlParameters( namespace, idQuery );
const { key } = parseId( idQuery, urlParameters );
- const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEM, { key } );
+ const itemQuery = getRequestIdentifier( CRUD_ACTIONS.GET_ITEM, key );
return state.errors[ itemQuery ];
};
export const getItems = createSelector(
( state: ResourceState, query?: ItemQuery ) => {
- const itemQuery = getResourceName(
+ const itemQuery = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS,
query || {}
);
@@ -96,7 +103,7 @@ export const getItems = createSelector(
return data;
},
( state, query ) => {
- const itemQuery = getResourceName(
+ const itemQuery = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS,
query || {}
);
@@ -129,7 +136,10 @@ export const getItemsTotalCount = (
};
export const getItemsError = ( state: ResourceState, query?: ItemQuery ) => {
- const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query || {} );
+ const itemQuery = getRequestIdentifier(
+ CRUD_ACTIONS.GET_ITEMS,
+ query || {}
+ );
return state.errors[ itemQuery ];
};
@@ -138,12 +148,8 @@ export const getItemUpdateError = (
idQuery: IdQuery,
urlParameters: IdType[]
) => {
- const params = parseId( idQuery, urlParameters );
- const { key } = params;
- const itemQuery = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, {
- key,
- params,
- } );
+ const { key } = parseId( idQuery, urlParameters );
+ const itemQuery = getRequestIdentifier( CRUD_ACTIONS.UPDATE_ITEM, key );
return state.errors[ itemQuery ];
};
@@ -154,6 +160,32 @@ export const createSelectors = ( {
pluralResourceName,
namespace,
}: SelectorOptions ) => {
+ const hasFinishedRequest = (
+ state: ResourceState,
+ action: string,
+ args = []
+ ) => {
+ const sanitizedArgs = maybeReplaceIdQuery( args, namespace );
+ const actionName = getGenericActionName( action, resourceName );
+ const requestId = getRequestIdentifier( actionName, ...sanitizedArgs );
+ if ( action )
+ return (
+ state.requesting.hasOwnProperty( requestId ) &&
+ ! state.requesting[ requestId ]
+ );
+ };
+
+ const isRequesting = (
+ state: ResourceState,
+ action: string,
+ args = []
+ ) => {
+ const sanitizedArgs = maybeReplaceIdQuery( args, namespace );
+ const actionName = getGenericActionName( action, resourceName );
+ const requestId = getRequestIdentifier( actionName, ...sanitizedArgs );
+ return state.requesting[ requestId ];
+ };
+
return {
[ `get${ resourceName }` ]: applyNamespace( getItem, namespace ),
[ `get${ resourceName }Error` ]: applyNamespace(
@@ -184,5 +216,7 @@ export const createSelectors = ( {
getItemUpdateError,
namespace
),
+ hasFinishedRequest,
+ isRequesting,
};
};
diff --git a/packages/js/data/src/crud/test/reducer.ts b/packages/js/data/src/crud/test/reducer.ts
index d0d3391e60a..0b2ec44c7c7 100644
--- a/packages/js/data/src/crud/test/reducer.ts
+++ b/packages/js/data/src/crud/test/reducer.ts
@@ -5,6 +5,7 @@ import { Actions } from '../actions';
import { createReducer, ResourceState } from '../reducer';
import { CRUD_ACTIONS } from '../crud-actions';
import { getResourceName } from '../../utils';
+import { getRequestIdentifier } from '..//utils';
import { Item, ItemQuery } from '../types';
import TYPES from '../action-types';
@@ -13,6 +14,7 @@ const defaultState: ResourceState = {
errors: {},
itemsCount: {},
data: {},
+ requesting: {},
};
const reducer = createReducer();
@@ -38,6 +40,7 @@ describe( 'crud reducer', () => {
1: { id: 1, name: 'Donkey', status: 'draft' },
2: { id: 2, name: 'Sauce', status: 'publish' },
},
+ requesting: {},
};
const update: Item = {
id: 2,
@@ -72,7 +75,10 @@ describe( 'crud reducer', () => {
urlParameters: [],
} );
- const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.GET_ITEMS,
+ query
+ );
expect( state.items[ resourceName ].data ).toHaveLength( 2 );
expect( state.items[ resourceName ].data.includes( 1 ) ).toBeTruthy();
@@ -108,7 +114,10 @@ describe( 'crud reducer', () => {
urlParameters: [ 5 ],
} );
- const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.GET_ITEMS,
+ query
+ );
expect( state.items[ resourceName ].data ).toHaveLength( 2 );
expect( state.data[ '5/1' ] ).toEqual( items[ 0 ] );
@@ -141,7 +150,10 @@ describe( 'crud reducer', () => {
urlParameters: [],
} );
- const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.GET_ITEMS,
+ query
+ );
expect( state.items[ resourceName ].data ).toHaveLength( 2 );
expect( state.items[ resourceName ].data.includes( 1 ) ).toBeTruthy();
@@ -157,7 +169,10 @@ describe( 'crud reducer', () => {
it( 'should handle GET_ITEMS_ERROR', () => {
const query: Partial< ItemQuery > = { status: 'draft' };
- const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEMS, query );
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.GET_ITEMS,
+ query
+ );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.GET_ITEMS_ERROR,
@@ -171,7 +186,7 @@ describe( 'crud reducer', () => {
it( 'should handle GET_ITEMS_TOTAL_COUNT_ERROR', () => {
const query: Partial< ItemQuery > = { status: 'draft' };
- const resourceName = getResourceName(
+ const resourceName = getRequestIdentifier(
CRUD_ACTIONS.GET_ITEMS_TOTAL_COUNT,
query
);
@@ -188,7 +203,7 @@ describe( 'crud reducer', () => {
it( 'should handle GET_ITEM_ERROR', () => {
const key = 3;
- const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { key } );
+ const resourceName = getRequestIdentifier( CRUD_ACTIONS.GET_ITEM, key );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.GET_ITEM_ERROR,
@@ -202,7 +217,7 @@ describe( 'crud reducer', () => {
it( 'should handle GET_ITEM_ERROR', () => {
const key = 3;
- const resourceName = getResourceName( CRUD_ACTIONS.GET_ITEM, { key } );
+ const resourceName = getRequestIdentifier( CRUD_ACTIONS.GET_ITEM, key );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.GET_ITEM_ERROR,
@@ -216,7 +231,10 @@ describe( 'crud reducer', () => {
it( 'should handle CREATE_ITEM_ERROR', () => {
const query = { name: 'Invalid product' };
- const resourceName = getResourceName( CRUD_ACTIONS.CREATE_ITEM, query );
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.CREATE_ITEM,
+ query
+ );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.CREATE_ITEM_ERROR,
@@ -226,22 +244,28 @@ describe( 'crud reducer', () => {
} );
expect( state.errors[ resourceName ] ).toBe( error );
+ expect( state.requesting[ resourceName ] ).toBe( false );
} );
it( 'should handle UPDATE_ITEM_ERROR', () => {
const key = 2;
- const resourceName = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, {
+ const query = { property: 'value' };
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.UPDATE_ITEM,
key,
- } );
+ query
+ );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.UPDATE_ITEM_ERROR,
key,
error,
errorType: CRUD_ACTIONS.UPDATE_ITEM,
+ query,
} );
expect( state.errors[ resourceName ] ).toBe( error );
+ expect( state.requesting[ resourceName ] ).toBe( false );
} );
it( 'should handle UPDATE_ITEM_SUCCESS', () => {
@@ -258,17 +282,25 @@ describe( 'crud reducer', () => {
1: { id: 1, name: 'Donkey', status: 'draft' },
2: { id: 2, name: 'Sauce', status: 'publish' },
},
+ requesting: {},
};
const item: Item = {
id: 2,
name: 'Holy smokes!',
status: 'draft',
};
+ const query = { property: 'value' };
+ const requestId = getRequestIdentifier(
+ CRUD_ACTIONS.UPDATE_ITEM,
+ item.id,
+ query
+ );
const state = reducer( initialState, {
type: TYPES.UPDATE_ITEM_SUCCESS,
key: item.id,
item,
+ query,
} );
expect( state.items ).toEqual( initialState.items );
@@ -278,6 +310,7 @@ describe( 'crud reducer', () => {
expect( state.data[ 2 ].id ).toEqual( initialState.data[ 2 ].id );
expect( state.data[ 2 ].title ).toEqual( initialState.data[ 2 ].title );
expect( state.data[ 2 ].name ).toEqual( item.name );
+ expect( state.requesting[ requestId ] ).toEqual( false );
} );
it( 'should handle CREATE_ITEM_SUCCESS', () => {
@@ -286,15 +319,26 @@ describe( 'crud reducer', () => {
name: 'Off the hook!',
status: 'draft',
};
+ const query = {
+ name: 'Off the hook!',
+ status: 'draft',
+ };
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.CREATE_ITEM,
+ item.id,
+ query
+ );
const state = reducer( defaultState, {
type: TYPES.CREATE_ITEM_SUCCESS,
key: item.id,
item,
+ query,
} );
expect( state.data[ 2 ].name ).toEqual( item.name );
expect( state.data[ 2 ].status ).toEqual( item.status );
+ expect( state.requesting[ resourceName ] ).toEqual( false );
} );
it( 'should handle DELETE_ITEM_SUCCESS', () => {
@@ -311,6 +355,7 @@ describe( 'crud reducer', () => {
1: { id: 1, name: 'Donkey', status: 'draft' },
2: { id: 2, name: 'Sauce', status: 'publish' },
},
+ requesting: {},
};
const item1Updated: Item = {
id: 1,
@@ -333,25 +378,35 @@ describe( 'crud reducer', () => {
item: item2Updated,
force: false,
} );
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.DELETE_ITEM,
+ item1Updated.id,
+ true
+ );
expect( state.errors ).toEqual( initialState.errors );
expect( state.data[ 1 ] ).toEqual( undefined );
expect( state.data[ 2 ].status ).toEqual( 'trash' );
+ expect( state.requesting[ resourceName ] ).toBe( false );
} );
it( 'should handle DELETE_ITEM_ERROR', () => {
const key = 2;
- const resourceName = getResourceName( CRUD_ACTIONS.DELETE_ITEM, {
+ const resourceName = getRequestIdentifier(
+ CRUD_ACTIONS.DELETE_ITEM,
key,
- } );
+ false
+ );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.DELETE_ITEM_ERROR,
key,
error,
errorType: CRUD_ACTIONS.DELETE_ITEM,
+ force: false,
} );
expect( state.errors[ resourceName ] ).toBe( error );
+ expect( state.requesting[ resourceName ] ).toBe( false );
} );
} );
diff --git a/packages/js/data/src/crud/test/selectors.ts b/packages/js/data/src/crud/test/selectors.ts
index d6f56cc67e9..d6a3ea0b7b4 100644
--- a/packages/js/data/src/crud/test/selectors.ts
+++ b/packages/js/data/src/crud/test/selectors.ts
@@ -11,7 +11,7 @@ const selectors = createSelectors( {
describe( 'crud selectors', () => {
it( 'should return methods for the default selectors', () => {
- expect( Object.keys( selectors ).length ).toEqual( 8 );
+ expect( Object.keys( selectors ).length ).toEqual( 10 );
expect( selectors ).toHaveProperty( 'getProduct' );
expect( selectors ).toHaveProperty( 'getProducts' );
expect( selectors ).toHaveProperty( 'getProductsTotalCount' );
@@ -20,5 +20,7 @@ describe( 'crud selectors', () => {
expect( selectors ).toHaveProperty( 'getProductCreateError' );
expect( selectors ).toHaveProperty( 'getProductDeleteError' );
expect( selectors ).toHaveProperty( 'getProductUpdateError' );
+ expect( selectors ).toHaveProperty( 'hasFinishedRequest' );
+ expect( selectors ).toHaveProperty( 'isRequesting' );
} );
} );
diff --git a/packages/js/data/src/crud/test/utils.ts b/packages/js/data/src/crud/test/utils.ts
index 1fb371c9369..d034c6fa69b 100644
--- a/packages/js/data/src/crud/test/utils.ts
+++ b/packages/js/data/src/crud/test/utils.ts
@@ -1,13 +1,18 @@
/**
* Internal dependencies
*/
+import CRUD_ACTIONS from '../crud-actions';
import {
applyNamespace,
cleanQuery,
+ getGenericActionName,
getKey,
getNamespaceKeys,
+ getRequestIdentifier,
getRestPath,
getUrlParameters,
+ maybeReplaceIdQuery,
+ isValidIdQuery,
parseId,
} from '../utils';
@@ -113,4 +118,104 @@ describe( 'utils', () => {
expect( params.other_attribute ).toBe( 'a' );
expect( params.my_attribute ).toBeUndefined();
} );
+
+ it( 'should get the request identifier with no arguments', () => {
+ const key = getRequestIdentifier( 'CREATE_ITEM' );
+ expect( key ).toBe( 'CREATE_ITEM/[]' );
+ } );
+
+ it( 'should get the request identifier with a single argument', () => {
+ const key = getRequestIdentifier( 'CREATE_ITEM', 'string_arg' );
+ expect( key ).toBe( 'CREATE_ITEM/["string_arg"]' );
+ } );
+
+ it( 'should get the request identifier with multiple arguments', () => {
+ const key = getRequestIdentifier( 'CREATE_ITEM', 'string_arg', {
+ object_property: 'object_value',
+ } );
+ expect( key ).toBe(
+ 'CREATE_ITEM/["string_arg","{"object_property":"object_value"}"]'
+ );
+ } );
+
+ it( 'should sort object properties in the request identifier', () => {
+ const key = getRequestIdentifier( 'CREATE_ITEM', 'string_arg', {
+ b: '2',
+ a: '1',
+ } );
+ expect( key ).toBe( 'CREATE_ITEM/["string_arg","{"a":"1","b":"2"}"]' );
+ } );
+
+ it( 'should directly return the action when the action does not match the resource name', () => {
+ const genercActionName = getGenericActionName(
+ 'createNonThing',
+ 'Thing'
+ );
+ expect( genercActionName ).toBe( 'createNonThing' );
+ } );
+
+ it( 'should get the generic create action name based on resource name', () => {
+ const genercActionName = getGenericActionName( 'createThing', 'Thing' );
+ expect( genercActionName ).toBe( CRUD_ACTIONS.CREATE_ITEM );
+ } );
+
+ it( 'should get the generic delete action name based on resource name', () => {
+ const genercActionName = getGenericActionName( 'deleteThing', 'Thing' );
+ expect( genercActionName ).toBe( CRUD_ACTIONS.DELETE_ITEM );
+ } );
+
+ it( 'should get the generic update action name based on resource name', () => {
+ const genercActionName = getGenericActionName( 'updateThing', 'Thing' );
+ expect( genercActionName ).toBe( CRUD_ACTIONS.UPDATE_ITEM );
+ } );
+
+ it( 'should return false when a valid ID query is not given', () => {
+ expect( isValidIdQuery( { some: 'data' }, '/my/namespace' ) ).toBe(
+ false
+ );
+ } );
+
+ it( 'should return true when a valid ID is passed in an object', () => {
+ expect( isValidIdQuery( { id: 22 }, '/my/namespace' ) ).toBe( true );
+ } );
+
+ it( 'should return true when a valid ID is passed directly', () => {
+ expect( isValidIdQuery( 22, '/my/namespace' ) ).toBe( true );
+ } );
+
+ it( 'should return false when additional non-ID properties are provided', () => {
+ expect( isValidIdQuery( { id: 22, other: 88 }, '/my/namespace' ) ).toBe(
+ false
+ );
+ } );
+
+ it( 'should return true when namespace ID properties are provided', () => {
+ expect(
+ isValidIdQuery(
+ { id: 22, parent_id: 88 },
+ '/my/{parent_id}/namespace/'
+ )
+ ).toBe( true );
+ } );
+
+ it( 'should replace the first argument when a valid ID query exists', () => {
+ const args = [ { id: 22, parent_id: 88 }, 'second' ];
+ const sanitizedArgs = maybeReplaceIdQuery(
+ args,
+ '/my/{parent_id}/namespace/'
+ );
+ expect( sanitizedArgs ).toEqual( [ '88/22', 'second' ] );
+ } );
+
+ it( 'should remain unchanged when the first argument is a string or number', () => {
+ const args = [ 'first', 'second' ];
+ const sanitizedArgs = maybeReplaceIdQuery( args, '/my/namespace/' );
+ expect( sanitizedArgs ).toEqual( args );
+ } );
+
+ it( 'should remain unchanged when the first argument is not a valid ID query', () => {
+ const args = [ { id: 22, parent_id: 88 }, 'second' ];
+ const sanitizedArgs = maybeReplaceIdQuery( args, '/my/namespace/' );
+ expect( sanitizedArgs ).toEqual( args );
+ } );
} );
diff --git a/packages/js/data/src/crud/utils.ts b/packages/js/data/src/crud/utils.ts
index 09147c8b027..97e8d1e57d7 100644
--- a/packages/js/data/src/crud/utils.ts
+++ b/packages/js/data/src/crud/utils.ts
@@ -6,6 +6,7 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
+import CRUD_ACTIONS from './crud-actions';
import { IdQuery, IdType, ItemQuery } from './types';
/**
@@ -148,6 +149,52 @@ export const getUrlParameters = (
return params;
};
+/**
+ * Check to see if an argument is a valid type of ID query.
+ *
+ * @param arg Unknow argument to check.
+ * @param namespace The namespace string
+ * @return boolean
+ */
+export const isValidIdQuery = ( arg: unknown, namespace: string ) => {
+ if ( typeof arg === 'string' || typeof arg === 'number' ) {
+ return true;
+ }
+
+ const validKeys = [ 'id', ...getNamespaceKeys( namespace ) ];
+
+ if (
+ arg &&
+ typeof arg === 'object' &&
+ arg.hasOwnProperty( 'id' ) &&
+ JSON.stringify( validKeys.sort() ) ===
+ JSON.stringify( Object.keys( arg ).sort() )
+ ) {
+ return true;
+ }
+
+ return false;
+};
+
+/**
+ * Replace the initial argument with a key if it's a valid ID query.
+ *
+ * @param args Args to check.
+ * @param namespace Namespace.
+ * @return Sanitized arguments.
+ */
+export const maybeReplaceIdQuery = ( args: unknown[], namespace: string ) => {
+ const [ firstArgument, ...rest ] = args;
+ if ( ! firstArgument || ! isValidIdQuery( firstArgument, namespace ) ) {
+ return args;
+ }
+
+ const urlParameters = getUrlParameters( namespace, firstArgument );
+ const { key } = parseId( firstArgument as IdQuery, urlParameters );
+
+ return [ key, ...rest ];
+};
+
/**
* Clean a query of all namespaced params.
*
@@ -168,3 +215,46 @@ export const cleanQuery = (
return cleaned;
};
+
+/**
+ * Get the identifier for a request provided its arguments.
+ *
+ * @param name Name of action or selector.
+ * @param args Arguments for the request.
+ * @return Key to identify the request.
+ */
+export const getRequestIdentifier = ( name: string, ...args: unknown[] ) => {
+ const suffix = JSON.stringify(
+ args.map( ( arg ) => {
+ if ( typeof arg === 'object' && arg !== null ) {
+ return JSON.stringify( arg, Object.keys( arg ).sort() );
+ }
+ return arg;
+ } )
+ ).replace( /\\"/g, '"' );
+
+ return name + '/' + suffix;
+};
+
+/**
+ * Get a generic action name from a resource action name if one exists.
+ *
+ * @param action Action name to check.
+ * @param resourceName Resurce name.
+ * @return Generic action name if one exists, otherwise the passed action name.
+ */
+export const getGenericActionName = (
+ action: string,
+ resourceName: string
+) => {
+ switch ( action ) {
+ case `create${ resourceName }`:
+ return CRUD_ACTIONS.CREATE_ITEM;
+ case `delete${ resourceName }`:
+ return CRUD_ACTIONS.DELETE_ITEM;
+ case `update${ resourceName }`:
+ return CRUD_ACTIONS.UPDATE_ITEM;
+ }
+
+ return action;
+};
From 497119b62273eda5a1b15d5367f81e37ad7508ab Mon Sep 17 00:00:00 2001
From: Mahdi Taleghani
Date: Mon, 16 Jan 2023 21:23:55 +0330
Subject: [PATCH 11/58] fixing a typo in i18n states file
---
plugins/woocommerce/i18n/states.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/woocommerce/i18n/states.php b/plugins/woocommerce/i18n/states.php
index b43d34cd698..d2cf0663755 100644
--- a/plugins/woocommerce/i18n/states.php
+++ b/plugins/woocommerce/i18n/states.php
@@ -815,7 +815,7 @@ return array(
'LD' => __( 'Lakshadeep', 'woocommerce' ),
'PY' => __( 'Pondicherry (Puducherry)', 'woocommerce' ),
),
- 'IR' => array( // Irania states.
+ 'IR' => array( // Iranian states.
'KHZ' => __( 'Khuzestan (خوزستان)', 'woocommerce' ),
'THR' => __( 'Tehran (تهران)', 'woocommerce' ),
'ILM' => __( 'Ilaam (ایلام)', 'woocommerce' ),
From 3719b62dad02f09755df14929e9a3dcd5213d608 Mon Sep 17 00:00:00 2001
From: Priyanka Behera
Date: Tue, 17 Jan 2023 01:01:48 +0530
Subject: [PATCH 12/58] Fixed - woocommerce_order_tracking shortcode causes
fatal error if a refund ID is entered #31760 issue (#33735)
* Fixed #31760 issue
* additional space removal after &&
* Make sure tracking shortcode only tracks orders of type `WC_Order`
* Add changelog
Co-authored-by: Jorge A. Torres
---
plugins/woocommerce/changelog/fix-31760 | 4 ++++
.../includes/shortcodes/class-wc-shortcode-order-tracking.php | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 plugins/woocommerce/changelog/fix-31760
diff --git a/plugins/woocommerce/changelog/fix-31760 b/plugins/woocommerce/changelog/fix-31760
new file mode 100644
index 00000000000..e389925f1a9
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-31760
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make sure the tracking shortcode only operates in orders with billing information.
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-order-tracking.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-order-tracking.php
index 6798539ae82..2151c69e9ab 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-order-tracking.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-order-tracking.php
@@ -51,7 +51,7 @@ class WC_Shortcode_Order_Tracking {
} else {
$order = wc_get_order( apply_filters( 'woocommerce_shortcode_order_tracking_order_id', $order_id ) );
- if ( $order && $order->get_id() && strtolower( $order->get_billing_email() ) === strtolower( $order_email ) ) {
+ if ( $order && $order->get_id() && is_a( $order, 'WC_Order' ) && strtolower( $order->get_billing_email() ) === strtolower( $order_email ) ) {
do_action( 'woocommerce_track_order', $order->get_id() );
wc_get_template(
'order/tracking.php',
From 8fc9c5cc0973816740fd9adec0988d646c2530c8 Mon Sep 17 00:00:00 2001
From: louwie17
Date: Mon, 16 Jan 2023 16:50:30 -0400
Subject: [PATCH 13/58] Add basic fields for rendering of Product MVP (#36392)
---
.../changelog/add-36074_basic_fields | 4 +
packages/js/components/src/index.ts | 1 +
.../src/product-fields/api/render.tsx | 9 +-
.../fields/basic-select-control/index.ts | 15 +++
.../fields/basic-select-control/render.tsx | 34 +++++++
.../product-fields/fields/checkbox/index.ts | 15 +++
.../product-fields/fields/checkbox/render.tsx | 28 ++++++
.../src/product-fields/fields/index.ts | 29 ++++++
.../src/product-fields/fields/radio/index.ts | 15 +++
.../product-fields/fields/radio/render.tsx | 34 +++++++
.../src/product-fields/fields/text/index.ts | 15 +++
.../src/product-fields/fields/text/render.tsx | 24 +++++
.../src/product-fields/fields/toggle/index.ts | 15 +++
.../product-fields/fields/toggle/render.tsx | 40 ++++++++
.../src/product-fields/fields/types.ts | 6 ++
.../js/components/src/product-fields/index.ts | 1 +
.../src/product-fields/store/types.ts | 4 +-
.../src/product-fields/stories/index.tsx | 97 ++++++++++++++-----
18 files changed, 357 insertions(+), 29 deletions(-)
create mode 100644 packages/js/components/changelog/add-36074_basic_fields
create mode 100644 packages/js/components/src/product-fields/fields/basic-select-control/index.ts
create mode 100644 packages/js/components/src/product-fields/fields/basic-select-control/render.tsx
create mode 100644 packages/js/components/src/product-fields/fields/checkbox/index.ts
create mode 100644 packages/js/components/src/product-fields/fields/checkbox/render.tsx
create mode 100644 packages/js/components/src/product-fields/fields/index.ts
create mode 100644 packages/js/components/src/product-fields/fields/radio/index.ts
create mode 100644 packages/js/components/src/product-fields/fields/radio/render.tsx
create mode 100644 packages/js/components/src/product-fields/fields/text/index.ts
create mode 100644 packages/js/components/src/product-fields/fields/text/render.tsx
create mode 100644 packages/js/components/src/product-fields/fields/toggle/index.ts
create mode 100644 packages/js/components/src/product-fields/fields/toggle/render.tsx
create mode 100644 packages/js/components/src/product-fields/fields/types.ts
diff --git a/packages/js/components/changelog/add-36074_basic_fields b/packages/js/components/changelog/add-36074_basic_fields
new file mode 100644
index 00000000000..cc93a0ca6c4
--- /dev/null
+++ b/packages/js/components/changelog/add-36074_basic_fields
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add 6 basic fields to the product fields registry for use in extensibility within the new Product MVP.
diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts
index 8330ed8b4f2..3430cb01281 100644
--- a/packages/js/components/src/index.ts
+++ b/packages/js/components/src/index.ts
@@ -87,3 +87,4 @@ export { CollapsibleContent } from './collapsible-content';
export { createOrderedChildren, sortFillsByOrder } from './utils';
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';
+export * from './product-fields';
diff --git a/packages/js/components/src/product-fields/api/render.tsx b/packages/js/components/src/product-fields/api/render.tsx
index 5fc727f08af..4ef208cad63 100644
--- a/packages/js/components/src/product-fields/api/render.tsx
+++ b/packages/js/components/src/product-fields/api/render.tsx
@@ -3,6 +3,10 @@
*/
import { select } from '@wordpress/data';
import { createElement } from '@wordpress/element';
+import {
+ // @ts-expect-error `__experimentalInputControl` does exist.
+ __experimentalInputControl as InputControl,
+} from '@wordpress/components';
/**
* Internal dependencies
@@ -19,10 +23,7 @@ export function renderField( name: string, props: Record< string, any > ) {
return ;
}
if ( fieldConfig.type ) {
- return createElement( 'input', {
- type: fieldConfig.type,
- ...props,
- } );
+ return ;
}
return null;
}
diff --git a/packages/js/components/src/product-fields/fields/basic-select-control/index.ts b/packages/js/components/src/product-fields/fields/basic-select-control/index.ts
new file mode 100644
index 00000000000..178d735e860
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/basic-select-control/index.ts
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { ComponentType } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { ProductFieldDefinition } from '../../store/types';
+import render from './render';
+
+export const basicSelectControlSettings: ProductFieldDefinition = {
+ name: 'basic-select-control',
+ render: render as ComponentType,
+};
diff --git a/packages/js/components/src/product-fields/fields/basic-select-control/render.tsx b/packages/js/components/src/product-fields/fields/basic-select-control/render.tsx
new file mode 100644
index 00000000000..b108cfb9b3d
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/basic-select-control/render.tsx
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { createElement } from '@wordpress/element';
+import { RadioControl, SelectControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { BaseProductFieldProps } from '../types';
+
+type SelectControlFieldProps = BaseProductFieldProps< string | string[] > & {
+ multiple?: boolean;
+ options: SelectControl.Option[];
+};
+const SelectControlField: React.FC< SelectControlFieldProps > = ( {
+ label,
+ value,
+ onChange,
+ multiple,
+ options = [],
+} ) => {
+ return (
+
+ );
+};
+
+export default SelectControlField;
diff --git a/packages/js/components/src/product-fields/fields/checkbox/index.ts b/packages/js/components/src/product-fields/fields/checkbox/index.ts
new file mode 100644
index 00000000000..b1f0d038176
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/checkbox/index.ts
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { ComponentType } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { ProductFieldDefinition } from '../../store/types';
+import render from './render';
+
+export const checkboxSettings: ProductFieldDefinition = {
+ name: 'checkbox',
+ render: render as ComponentType,
+};
diff --git a/packages/js/components/src/product-fields/fields/checkbox/render.tsx b/packages/js/components/src/product-fields/fields/checkbox/render.tsx
new file mode 100644
index 00000000000..4a82e8e3b19
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/checkbox/render.tsx
@@ -0,0 +1,28 @@
+/**
+ * External dependencies
+ */
+import { createElement } from '@wordpress/element';
+import { CheckboxControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { BaseProductFieldProps } from '../types';
+
+type CheckboxFieldProps = BaseProductFieldProps< boolean >;
+
+const CheckboxField: React.FC< CheckboxFieldProps > = ( {
+ label,
+ value,
+ onChange,
+} ) => {
+ return (
+
+ );
+};
+
+export default CheckboxField;
diff --git a/packages/js/components/src/product-fields/fields/index.ts b/packages/js/components/src/product-fields/fields/index.ts
new file mode 100644
index 00000000000..2ccb3c83653
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/index.ts
@@ -0,0 +1,29 @@
+/**
+ * Internal dependencies
+ */
+import { registerProductField } from '../api';
+import { ProductFieldDefinition } from '../store/types';
+import { basicSelectControlSettings } from './basic-select-control';
+import { checkboxSettings } from './checkbox';
+import { radioSettings } from './radio';
+import { textSettings } from './text';
+import { toggleSettings } from './toggle';
+
+const getAllProductFields = (): ProductFieldDefinition[] =>
+ [
+ ...[ 'number' ].map( ( type ) => ( {
+ name: type,
+ type,
+ } ) ),
+ textSettings,
+ toggleSettings,
+ radioSettings,
+ basicSelectControlSettings,
+ checkboxSettings,
+ ].filter( Boolean );
+
+export const registerCoreProductFields = ( fields = getAllProductFields() ) => {
+ fields.forEach( ( field ) => {
+ registerProductField( field.name, field );
+ } );
+};
diff --git a/packages/js/components/src/product-fields/fields/radio/index.ts b/packages/js/components/src/product-fields/fields/radio/index.ts
new file mode 100644
index 00000000000..db56c058126
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/radio/index.ts
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { ComponentType } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { ProductFieldDefinition } from '../../store/types';
+import render from './render';
+
+export const radioSettings: ProductFieldDefinition = {
+ name: 'radio',
+ render: render as ComponentType,
+};
diff --git a/packages/js/components/src/product-fields/fields/radio/render.tsx b/packages/js/components/src/product-fields/fields/radio/render.tsx
new file mode 100644
index 00000000000..0577f10fe44
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/radio/render.tsx
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { createElement } from '@wordpress/element';
+import { RadioControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { BaseProductFieldProps } from '../types';
+
+type RadioFieldProps = BaseProductFieldProps< string > & {
+ options: {
+ label: string;
+ value: string;
+ }[];
+};
+const RadioField: React.FC< RadioFieldProps > = ( {
+ label,
+ value,
+ onChange,
+ options = [],
+} ) => {
+ return (
+
+ );
+};
+
+export default RadioField;
diff --git a/packages/js/components/src/product-fields/fields/text/index.ts b/packages/js/components/src/product-fields/fields/text/index.ts
new file mode 100644
index 00000000000..5dd23abeb02
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/text/index.ts
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { ComponentType } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { ProductFieldDefinition } from '../../store/types';
+import render from './render';
+
+export const textSettings: ProductFieldDefinition = {
+ name: 'text',
+ render: render as ComponentType,
+};
diff --git a/packages/js/components/src/product-fields/fields/text/render.tsx b/packages/js/components/src/product-fields/fields/text/render.tsx
new file mode 100644
index 00000000000..ca441faeac5
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/text/render.tsx
@@ -0,0 +1,24 @@
+/**
+ * External dependencies
+ */
+import { createElement } from '@wordpress/element';
+import { TextControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { BaseProductFieldProps } from '../types';
+
+type TextFieldProps = BaseProductFieldProps< string >;
+
+const TextField: React.FC< TextFieldProps > = ( {
+ label,
+ value,
+ onChange,
+} ) => {
+ return (
+
+ );
+};
+
+export default TextField;
diff --git a/packages/js/components/src/product-fields/fields/toggle/index.ts b/packages/js/components/src/product-fields/fields/toggle/index.ts
new file mode 100644
index 00000000000..22d2aa2dd0f
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/toggle/index.ts
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { ComponentType } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import { ProductFieldDefinition } from '../../store/types';
+import render from './render';
+
+export const toggleSettings: ProductFieldDefinition = {
+ name: 'toggle',
+ render: render as ComponentType,
+};
diff --git a/packages/js/components/src/product-fields/fields/toggle/render.tsx b/packages/js/components/src/product-fields/fields/toggle/render.tsx
new file mode 100644
index 00000000000..bbfa33c6e17
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/toggle/render.tsx
@@ -0,0 +1,40 @@
+/**
+ * External dependencies
+ */
+import { createElement, Fragment } from '@wordpress/element';
+import { ToggleControl } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { BaseProductFieldProps } from '../types';
+import { Tooltip } from '../../../tooltip';
+
+type ToggleFieldProps = BaseProductFieldProps< boolean > & {
+ tooltip?: string;
+};
+const ToggleField: React.FC< ToggleFieldProps > = ( {
+ label,
+ value,
+ onChange,
+ tooltip,
+ disabled = false,
+} ) => {
+ return (
+
+ { label }
+ { tooltip && }
+ >
+ }
+ checked={ value }
+ onChange={ onChange }
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore disabled prop exists
+ disabled={ disabled }
+ />
+ );
+};
+
+export default ToggleField;
diff --git a/packages/js/components/src/product-fields/fields/types.ts b/packages/js/components/src/product-fields/fields/types.ts
new file mode 100644
index 00000000000..c8ed482089b
--- /dev/null
+++ b/packages/js/components/src/product-fields/fields/types.ts
@@ -0,0 +1,6 @@
+export type BaseProductFieldProps< T > = {
+ value: T;
+ onChange: ( value: T ) => void;
+ label: string;
+ disabled?: boolean;
+};
diff --git a/packages/js/components/src/product-fields/index.ts b/packages/js/components/src/product-fields/index.ts
index a28dcc4ff74..30d0c89835a 100644
--- a/packages/js/components/src/product-fields/index.ts
+++ b/packages/js/components/src/product-fields/index.ts
@@ -1,2 +1,3 @@
export { store } from './store';
export * from './api';
+export * from './fields';
diff --git a/packages/js/components/src/product-fields/store/types.ts b/packages/js/components/src/product-fields/store/types.ts
index c5de06cb663..ebab49c0509 100644
--- a/packages/js/components/src/product-fields/store/types.ts
+++ b/packages/js/components/src/product-fields/store/types.ts
@@ -1,11 +1,11 @@
/**
* External dependencies
*/
-import { ComponentType } from 'react';
+import { ComponentType, HTMLInputTypeAttribute } from 'react';
export type ProductFieldDefinition = {
name: string;
- type?: string;
+ type?: HTMLInputTypeAttribute;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render?: ComponentType;
};
diff --git a/packages/js/components/src/product-fields/stories/index.tsx b/packages/js/components/src/product-fields/stories/index.tsx
index 04496d889ca..4abf6631d54 100644
--- a/packages/js/components/src/product-fields/stories/index.tsx
+++ b/packages/js/components/src/product-fields/stories/index.tsx
@@ -3,56 +3,92 @@
*/
import React from 'react';
import { useState, createElement } from '@wordpress/element';
-import { createRegistry, RegistryProvider, select } from '@wordpress/data';
-import {
- // @ts-expect-error `__experimentalInputControl` does exist.
- __experimentalInputControl as InputControl,
-} from '@wordpress/components';
+import { createRegistry, RegistryProvider } from '@wordpress/data';
/**
* Internal dependencies
*/
import { store } from '../store';
-import { registerProductField, renderField } from '../api';
+import { renderField } from '../api';
+import { registerCoreProductFields } from '../fields';
const registry = createRegistry();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
registry.register( store );
-registerProductField( 'text', {
- name: 'text',
- render: ( props ) => {
- return ;
- },
-} );
+registerCoreProductFields();
-registerProductField( 'number', {
- name: 'number',
- render: () => {
- return ;
+const fieldConfigs = [
+ {
+ name: 'text-field',
+ type: 'text',
+ label: 'Text field',
},
-} );
+ {
+ name: 'number-field',
+ type: 'number',
+ label: 'Number field',
+ },
+ {
+ name: 'toggle-field',
+ type: 'toggle',
+ label: 'Toggle field',
+ },
+ {
+ name: 'checkbox-field',
+ type: 'checkbox',
+ label: 'Checkbox field',
+ },
+ {
+ name: 'radio-field',
+ type: 'radio',
+ label: 'Radio field',
+ options: [
+ { label: 'Option', value: 'option' },
+ { label: 'Option 2', value: 'option2' },
+ { label: 'Option 3', value: 'option3' },
+ ],
+ },
+ {
+ name: 'basic-select-control-field',
+ type: 'basic-select-control',
+ label: 'Basic select control field',
+ options: [
+ { label: 'Option', value: 'option' },
+ { label: 'Option 2', value: 'option2' },
+ { label: 'Option 3', value: 'option3' },
+ ],
+ },
+];
const RenderField = () => {
- const fields: string[] = select( store ).getRegisteredProductFields();
const [ selectedField, setSelectedField ] = useState(
- fields ? fields[ 0 ] : undefined
+ fieldConfigs[ 0 ].name || undefined
);
+ const [ value, setValue ] = useState();
const handleChange = ( event ) => {
setSelectedField( event.target.value );
};
+ const selectedFieldConfig = fieldConfigs.find(
+ ( f ) => f.name === selectedField
+ );
return (
- { fields.map( ( field ) => (
-
- { field }
+ { fieldConfigs.map( ( field ) => (
+
+ { field.label }
) ) }
- { selectedField && renderField( selectedField, { name: 'test' } ) }
+ { selectedFieldConfig &&
+ renderField( selectedFieldConfig.type, {
+ value,
+ onChange: setValue,
+ ...selectedFieldConfig,
+ } ) }
);
};
@@ -65,6 +101,21 @@ export const Basic: React.FC = () => {
);
};
+export const ToggleWithTooltip: React.FC = () => {
+ const [ value, setValue ] = useState();
+ return (
+
+ { renderField( 'toggle', {
+ value,
+ onChange: setValue,
+ name: 'toggle',
+ label: 'Toggle with Tooltip',
+ tooltip: 'This is a sample tooltip',
+ } ) }
+
+ );
+};
+
export default {
title: 'WooCommerce Admin/experimental/product-fields',
component: Basic,
From 6377314b1bb9f516e7e146bdd66a19702a3437df Mon Sep 17 00:00:00 2001
From: Willington Vega
Date: Mon, 16 Jan 2023 21:47:00 -0500
Subject: [PATCH 14/58] Remove deprecated usage of `${var}` syntax in strings
(#36439)
* issue-35763/fix-php-8.2-deprecation-warnings
* Declare $mockable_functions property
* Declare $mockable_classes property
* Fix deprecated usage of ${var} in strings
* Add changelog file
* Avoid using interpolation to create SQL statement
We could ignore the PHPCS error. However, ignoring the error leaves
PHPCS unable to detect future changes that may introduce unsafe
interpolation.
I think the more verbose approach is the safest approach in this
case.
* Ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
There doesn't seem to be a way to use a variable name for the name of the table without
triggering a PHPCS error.
* Avoid interpolated passing interpolated variables to __()
* End inline comments with a full-stop
---
.../fix-deprecated-usage-of-var-syntax-in-strings | 4 ++++
.../settings/class-wc-settings-payment-gateways.php | 2 +-
.../admin/views/html-admin-page-status-tools.php | 2 +-
plugins/woocommerce/includes/wc-core-functions.php | 7 +++++--
.../src/Admin/API/Reports/Orders/DataStore.php | 2 +-
.../src/Admin/API/Reports/Orders/Stats/DataStore.php | 7 +++++--
.../Admin/API/Reports/Products/Stats/DataStore.php | 2 +-
.../Admin/API/Reports/Variations/Stats/DataStore.php | 2 +-
.../src/Admin/Features/Navigation/Menu.php | 4 ++--
.../Orders/CustomOrdersTableController.php | 12 ++++++++++--
.../src/Internal/DownloadPermissionsAdjuster.php | 2 +-
.../src/Internal/Features/FeaturesController.php | 2 +-
plugins/woocommerce/src/Utilities/FeaturesUtil.php | 2 +-
.../Tools/CodeHacking/Hacks/FunctionsMockerHack.php | 7 +++++++
.../Tools/CodeHacking/Hacks/StaticMockerHack.php | 6 ++++++
.../admin/class-wc-admin-dashboard-setup-test.php | 4 ++--
.../includes/settings/class-wc-settings-example.php | 2 +-
.../Internal/Traits/AccessiblePrivateMethodsTest.php | 4 ++--
tools/monorepo/check-changelogger-use.php | 4 ++--
19 files changed, 54 insertions(+), 23 deletions(-)
create mode 100644 plugins/woocommerce/changelog/fix-deprecated-usage-of-var-syntax-in-strings
diff --git a/plugins/woocommerce/changelog/fix-deprecated-usage-of-var-syntax-in-strings b/plugins/woocommerce/changelog/fix-deprecated-usage-of-var-syntax-in-strings
new file mode 100644
index 00000000000..16c76f1a5dc
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-deprecated-usage-of-var-syntax-in-strings
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix deprecated usage of ${var} in strings
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php
index a519ce26148..809057cc6f3 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-payment-gateways.php
@@ -267,7 +267,7 @@ class WC_Settings_Payment_Gateways extends WC_Settings_Page {
foreach ( $plugin_suggestions as $plugin_suggestion ) {
$alt = str_replace( '.png', '', basename( $plugin_suggestion->image_72x72 ) );
// phpcs:ignore
- echo " ";
+ echo " ";
}
echo '& more.';
}
diff --git a/plugins/woocommerce/includes/admin/views/html-admin-page-status-tools.php b/plugins/woocommerce/includes/admin/views/html-admin-page-status-tools.php
index 31489a6ccdd..8da221e34ff 100644
--- a/plugins/woocommerce/includes/admin/views/html-admin-page-status-tools.php
+++ b/plugins/woocommerce/includes/admin/views/html-admin-page-status-tools.php
@@ -39,7 +39,7 @@ foreach ( $tools as $action_name => $tool ) {
echo wp_kses_post( $selector['description'] );
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- echo " ";
+ echo " ";
}
?>
diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php
index 18319fc1230..d4bbe5c944f 100644
--- a/plugins/woocommerce/includes/wc-core-functions.php
+++ b/plugins/woocommerce/includes/wc-core-functions.php
@@ -1701,8 +1701,11 @@ function wc_get_shipping_method_count( $include_legacy = false, $enabled_only =
return absint( $transient_value['value'] );
}
- $where_clause = $enabled_only ? 'WHERE is_enabled=1' : '';
- $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods ${where_clause}" ) );
+ if ( $enabled_only ) {
+ $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE is_enabled=1" ) );
+ } else {
+ $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" ) );
+ }
if ( $include_legacy ) {
// Count activated methods that don't support shipping zones.
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
index b3bc39a3048..741e2a5d2d1 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/DataStore.php
@@ -118,7 +118,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
if ( $query_args['customer_type'] ) {
$returning_customer = 'returning' === $query_args['customer_type'] ? 1 : 0;
- $where_subquery[] = "{$order_stats_lookup_table}.returning_customer = ${returning_customer}";
+ $where_subquery[] = "{$order_stats_lookup_table}.returning_customer = {$returning_customer}";
}
$refund_subquery = $this->get_refund_subquery( $query_args );
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
index b4661107704..b2ef6217df0 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Orders/Stats/DataStore.php
@@ -378,7 +378,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$this->update_intervals_sql_params( $query_args, $db_interval_count, $expected_interval_count, $table_name );
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
- $this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
+ $this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
@@ -697,7 +697,10 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
$wpdb->query(
$wpdb->prepare(
- "UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
+ // phpcs:ignore Generic.Commenting.Todo.TaskFound
+ // TODO: use the %i placeholder to prepare the table name when available in the the minimum required WordPress version.
+ // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ "UPDATE {$orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
$order_id,
$customer_id
)
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php
index ec3513938fb..6883d35a891 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Products/Stats/DataStore.php
@@ -188,7 +188,7 @@ class DataStore extends ProductsDataStore implements DataStoreInterface {
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
- $this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
+ $this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
diff --git a/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/DataStore.php b/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/DataStore.php
index 5ea01ef6cd8..0b03d0df6ce 100644
--- a/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/DataStore.php
+++ b/plugins/woocommerce/src/Admin/API/Reports/Variations/Stats/DataStore.php
@@ -226,7 +226,7 @@ class DataStore extends VariationsDataStore implements DataStoreInterface {
$this->interval_query->add_sql_clause( 'order_by', $this->get_sql_clause( 'order_by' ) );
$this->interval_query->add_sql_clause( 'limit', $this->get_sql_clause( 'limit' ) );
- $this->interval_query->add_sql_clause( 'select', ", MAX(${table_name}.date_created) AS datetime_anchor" );
+ $this->interval_query->add_sql_clause( 'select', ", MAX({$table_name}.date_created) AS datetime_anchor" );
if ( '' !== $selections ) {
$this->interval_query->add_sql_clause( 'select', ', ' . $selections );
}
diff --git a/plugins/woocommerce/src/Admin/Features/Navigation/Menu.php b/plugins/woocommerce/src/Admin/Features/Navigation/Menu.php
index 35ef7c80b9d..27d540fb6e2 100644
--- a/plugins/woocommerce/src/Admin/Features/Navigation/Menu.php
+++ b/plugins/woocommerce/src/Admin/Features/Navigation/Menu.php
@@ -463,10 +463,10 @@ class Menu {
? "&post_type={$taxonomy_object->object_type[0]}"
: '';
$match_expression = 'term.php'; // Match term.php pages.
- $match_expression .= "(?=.*[?|&]taxonomy=${taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.
+ $match_expression .= "(?=.*[?|&]taxonomy={$taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.
$match_expression .= '|'; // Or.
$match_expression .= 'edit-tags.php'; // Match edit-tags.php pages.
- $match_expression .= "(?=.*[?|&]taxonomy=${taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.
+ $match_expression .= "(?=.*[?|&]taxonomy={$taxonomy}(&|$|#))"; // Lookahead to match a taxonomy URL param.
return array(
'default' => array_merge(
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
index d38305c2e8c..3ceeb399229 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/CustomOrdersTableController.php
@@ -146,7 +146,11 @@ class CustomOrdersTableController {
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
wc_doing_it_wrong(
$class_and_method,
- __( "${class_and_method}: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.", 'woocommerce' ),
+ sprintf(
+ // translators: %1$s the name of the class and method used.
+ __( '%1$s: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.', 'woocommerce' ),
+ $class_and_method
+ ),
'7.0'
);
}
@@ -160,7 +164,11 @@ class CustomOrdersTableController {
$class_and_method = ( new \ReflectionClass( $this ) )->getShortName() . '::' . __FUNCTION__;
wc_doing_it_wrong(
$class_and_method,
- __( "${class_and_method}: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.", 'woocommerce' ),
+ sprintf(
+ // translators: %1$s the name of the class and method used.
+ __( '%1$s: The visibility of the custom orders table feature is now handled by the WooCommerce features engine. See the FeaturesController class, or go to WooCommerce - Settings - Advanced - Features.', 'woocommerce' ),
+ $class_and_method
+ ),
'7.0'
);
}
diff --git a/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php b/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
index c2bbac1c030..a30c504e63e 100644
--- a/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
+++ b/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
@@ -154,7 +154,7 @@ class DownloadPermissionsAdjuster {
'file' => $file,
'data' => (array) $permission->data,
);
- $result['permission_data_by_file_order_user'][ "${file}:${permission_data['user_id']}:${permission_data['order_id']}" ] = $data;
+ $result['permission_data_by_file_order_user'][ "{$file}:{$permission_data['user_id']}:{$permission_data['order_id']}" ] = $data;
}
}
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 5fe17a80970..9adc5508ce6 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -404,7 +404,7 @@ class FeaturesController {
return NewProductManagementExperience::TOGGLE_OPTION_NAME;
}
- return "woocommerce_feature_${feature_id}_enabled";
+ return "woocommerce_feature_{$feature_id}_enabled";
}
/**
diff --git a/plugins/woocommerce/src/Utilities/FeaturesUtil.php b/plugins/woocommerce/src/Utilities/FeaturesUtil.php
index 6acf0f29918..0eedde641f4 100644
--- a/plugins/woocommerce/src/Utilities/FeaturesUtil.php
+++ b/plugins/woocommerce/src/Utilities/FeaturesUtil.php
@@ -59,7 +59,7 @@ class FeaturesUtil {
if ( ! $plugin_id ) {
$logger = wc_get_logger();
- $logger->error( "FeaturesUtil::declare_compatibility: ${plugin_file} is not a known WordPress plugin." );
+ $logger->error( "FeaturesUtil::declare_compatibility: {$plugin_file} is not a known WordPress plugin." );
return false;
}
diff --git a/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php
index 91f6b3e535a..cf3fc83c6ab 100644
--- a/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php
+++ b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php
@@ -35,6 +35,13 @@ use ReflectionClass;
* executed inside tests (and thus the above example won't stack-overflow).
*/
final class FunctionsMockerHack extends CodeHack {
+ /**
+ * An array containing the names of the functions that will become mockable.
+ *
+ * @var array
+ */
+ private $mockable_functions;
+
/**
* Tokens that precede a non-standalone-function identifier.
*
diff --git a/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php
index 61fd4a68d90..65b5b334aca 100644
--- a/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php
+++ b/plugins/woocommerce/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php
@@ -35,6 +35,12 @@ namespace Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks;
* executed inside tests (and thus the above example won't stack-overflow).
*/
final class StaticMockerHack extends CodeHack {
+ /**
+ * An associative array of class name => array of class methods.
+ *
+ * @var array
+ */
+ private $mockable_classes;
/**
* @var StaticMockerHack Holds the only existing instance of the class.
diff --git a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
index 1ef4c8c4ae6..637f4cdeaf5 100644
--- a/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
+++ b/plugins/woocommerce/tests/php/includes/admin/class-wc-admin-dashboard-setup-test.php
@@ -165,7 +165,7 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
);
foreach ( $required_strings as $required_string ) {
- $this->assertRegexp( "/${required_string}/", $html );
+ $this->assertRegexp( "/{$required_string}/", $html );
}
}
@@ -191,7 +191,7 @@ class WC_Admin_Dashboard_Setup_Test extends WC_Unit_Test_Case {
if ( $completed_tasks_count === $tasks_count ) {
$this->assertEmpty( $this->get_widget_output() );
} else {
- $this->assertRegexp( "/Step ${step_number} of 6/", $this->get_widget_output() );
+ $this->assertRegexp( "/Step {$step_number} of 6/", $this->get_widget_output() );
}
}
}
diff --git a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-example.php b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-example.php
index 49a0bfbf965..a685a6f514c 100644
--- a/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-example.php
+++ b/plugins/woocommerce/tests/php/includes/settings/class-wc-settings-example.php
@@ -26,7 +26,7 @@ class WC_Settings_Example extends WC_Settings_Page {
}
protected function get_settings_for_section_core( $section_id ) {
- return array( "${section_id}_key" => "${section_id}_value" );
+ return array( "{$section_id}_key" => "{$section_id}_value" );
}
protected function get_own_sections() {
diff --git a/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php b/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php
index 32a14ea7297..31679b6ac25 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Traits/AccessiblePrivateMethodsTest.php
@@ -419,8 +419,8 @@ class AccessiblePrivateMethodsTest extends \WC_Unit_Test_Case {
use AccessiblePrivateMethods;
};
- $method_name = "add_${action_or_filter}";
- $proper_method_name = "add_static_${action_or_filter}";
+ $method_name = "add_{$action_or_filter}";
+ $proper_method_name = "add_static_{$action_or_filter}";
$this->expectException( \Error::class );
$this->expectExceptionMessage( get_class( $sut ) . '::' . "$method_name can't be called statically, did you mean '$proper_method_name'?" );
diff --git a/tools/monorepo/check-changelogger-use.php b/tools/monorepo/check-changelogger-use.php
index b2819ef2a48..b9f07dd3c83 100644
--- a/tools/monorepo/check-changelogger-use.php
+++ b/tools/monorepo/check-changelogger-use.php
@@ -70,9 +70,9 @@ if ( $verbose ) {
*/
function debug( ...$args ) {
if ( getenv( 'CI' ) ) {
- $args[0] = "\e[34m${args[0]}\e[0m\n";
+ $args[0] = "\e[34m{$args[0]}\e[0m\n";
} else {
- $args[0] = "\e[1;30m${args[0]}\e[0m\n";
+ $args[0] = "\e[1;30m{$args[0]}\e[0m\n";
}
fprintf( STDERR, ...$args );
}
From ea64a98f54b1859f161b5d9a538de06c88a5fbb1 Mon Sep 17 00:00:00 2001
From: Joshua T Flowers
Date: Tue, 17 Jan 2023 00:34:08 -0800
Subject: [PATCH 15/58] Extract attribute filtering and fetching logic out of
attribute components (#36354)
* Move attribution fetching to separate hook
* Add changelog entry
* Set all attributes on update of subset of attributes
* Move filtering logic to hook
* Remove tests that filter attribute by type inside the component
* Rename AttributeField to AttributeControl and props from attributes to value
---
.../add-attribute-modal.scss | 0
.../add-attribute-modal.tsx | 10 +-
.../attribute-control.tsx} | 149 ++++--------------
.../attribute-field.scss | 0
.../edit-attribute-modal.scss | 0
.../edit-attribute-modal.tsx | 18 +--
.../fields/attribute-control/index.ts | 1 +
.../test/add-attribute-modal.spec.tsx | 0
.../test/attribute-field.spec.tsx | 29 ++--
.../test/utils.spec.ts | 0
.../utils.ts | 0
.../products/fields/attribute-field/index.ts | 1 -
.../attribute-input-field.tsx | 4 +-
.../products/fields/attributes/attributes.tsx | 16 +-
.../products/fields/options/options.tsx | 24 ++-
.../products/hooks/use-product-attributes.ts | 111 +++++++++++++
plugins/woocommerce/changelog/update-36304 | 4 +
17 files changed, 201 insertions(+), 166 deletions(-)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/add-attribute-modal.scss (100%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/add-attribute-modal.tsx (97%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field/attribute-field.tsx => attribute-control/attribute-control.tsx} (65%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/attribute-field.scss (100%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/edit-attribute-modal.scss (100%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/edit-attribute-modal.tsx (87%)
create mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-control/index.ts
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/test/add-attribute-modal.spec.tsx (100%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/test/attribute-field.spec.tsx (90%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/test/utils.spec.ts (100%)
rename plugins/woocommerce-admin/client/products/fields/{attribute-field => attribute-control}/utils.ts (100%)
delete mode 100644 plugins/woocommerce-admin/client/products/fields/attribute-field/index.ts
create mode 100644 plugins/woocommerce-admin/client/products/hooks/use-product-attributes.ts
create mode 100644 plugins/woocommerce/changelog/update-36304
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-control/add-attribute-modal.scss
similarity index 100%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.scss
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/add-attribute-modal.scss
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/add-attribute-modal.tsx
similarity index 97%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/add-attribute-modal.tsx
index cf5c61288c8..ea4c9ae9b2f 100644
--- a/plugins/woocommerce-admin/client/products/fields/attribute-field/add-attribute-modal.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/add-attribute-modal.tsx
@@ -26,7 +26,7 @@ import {
AttributeTermInputField,
CustomAttributeTermInputField,
} from '../attribute-term-input-field';
-import { HydratedAttributeType } from '../attribute-field';
+import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import { getProductAttributeObject } from './utils';
type AddAttributeModalProps = {
@@ -46,12 +46,12 @@ type AddAttributeModalProps = {
confirmCancelLabel?: string;
confirmConfirmLabel?: string;
onCancel: () => void;
- onAdd: ( newCategories: HydratedAttributeType[] ) => void;
+ onAdd: ( newCategories: EnhancedProductAttribute[] ) => void;
selectedAttributeIds?: number[];
};
type AttributeForm = {
- attributes: Array< HydratedAttributeType | null >;
+ attributes: Array< EnhancedProductAttribute | null >;
};
export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
@@ -92,7 +92,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
};
const onAddingAttributes = ( values: AttributeForm ) => {
- const newAttributesToAdd: HydratedAttributeType[] = [];
+ const newAttributesToAdd: EnhancedProductAttribute[] = [];
values.attributes.forEach( ( attr ) => {
if (
attr !== null &&
@@ -105,7 +105,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
? ( attr.terms || [] ).map( ( term ) => term.name )
: attr.options;
newAttributesToAdd.push( {
- ...( attr as HydratedAttributeType ),
+ ...( attr as EnhancedProductAttribute ),
options,
} );
}
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx
similarity index 65%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx
index 7c867af4a0d..01e03c2f89b 100644
--- a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-control.tsx
@@ -2,13 +2,8 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
-import { useState, useCallback, useEffect } from '@wordpress/element';
-import {
- ProductAttribute,
- EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
- ProductAttributeTerm,
-} from '@woocommerce/data';
-import { resolveSelect } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+import { ProductAttribute } from '@woocommerce/data';
import {
Sortable,
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
@@ -24,42 +19,31 @@ import { getAdminLink } from '@woocommerce/settings';
import './attribute-field.scss';
import { AddAttributeModal } from './add-attribute-modal';
import { EditAttributeModal } from './edit-attribute-modal';
+import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import {
getAttributeKey,
reorderSortableProductAttributePositions,
} from './utils';
-import { sift } from '../../../utils';
import { AttributeEmptyState } from '../attribute-empty-state';
import {
AddAttributeListItem,
AttributeListItem,
} from '../attribute-list-item';
-type AttributeFieldProps = {
+type AttributeControlProps = {
value: ProductAttribute[];
onChange: ( value: ProductAttribute[] ) => void;
- productId?: number;
// TODO: should we support an 'any' option to show all attributes?
attributeType?: 'regular' | 'for-variations';
};
-export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & {
- options?: string[];
- terms?: ProductAttributeTerm[];
- visible?: boolean;
-};
-
-export const AttributeField: React.FC< AttributeFieldProps > = ( {
+export const AttributeControl: React.FC< AttributeControlProps > = ( {
value,
- onChange,
- productId,
attributeType = 'regular',
+ onChange,
} ) => {
const [ showAddAttributeModal, setShowAddAttributeModal ] =
useState( false );
- const [ hydratedAttributes, setHydratedAttributes ] = useState<
- HydratedAttributeType[]
- >( [] );
const [ editingAttributeId, setEditingAttributeId ] = useState<
null | string
>( null );
@@ -72,73 +56,12 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
? 'product_add_options_modal_cancel_button_click'
: 'product_add_attributes_modal_cancel_button_click';
- const fetchTerms = useCallback(
- ( attributeId: number ) => {
- return resolveSelect(
- EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
- )
- .getProductAttributeTerms< ProductAttributeTerm[] >( {
- attribute_id: attributeId,
- product: productId,
- } )
- .then(
- ( attributeTerms ) => {
- return attributeTerms;
- },
- ( error ) => {
- return error;
- }
- );
- },
- [ productId ]
- );
-
- useEffect( () => {
- // I think we'll need to move the hydration out of the individual component
- // instance. To where, I do not yet know... maybe in the form context
- // somewhere so that a single hydration source can be shared between multiple
- // instances? Something like a simple key-value store in the form context
- // would be handy.
- if ( ! value || hydratedAttributes.length !== 0 ) {
- return;
- }
-
- const [ customAttributes, globalAttributes ]: ProductAttribute[][] =
- sift( value, ( attr: ProductAttribute ) => attr.id === 0 );
-
- Promise.all(
- globalAttributes.map( ( attr ) => fetchTerms( attr.id ) )
- ).then( ( allResults ) => {
- setHydratedAttributes( [
- ...globalAttributes.map( ( attr, index ) => {
- const fetchedTerms = allResults[ index ];
-
- const newAttr = {
- ...attr,
- // I'm not sure this is quite right for handling unpersisted terms,
- // but this gets things kinda working for now
- terms:
- fetchedTerms.length > 0 ? fetchedTerms : undefined,
- options:
- fetchedTerms.length === 0
- ? attr.options
- : undefined,
- };
-
- return newAttr;
- } ),
- ...customAttributes,
- ] );
- } );
- }, [ fetchTerms, hydratedAttributes, value ] );
-
const fetchAttributeId = ( attribute: { id: number; name: string } ) =>
`${ attribute.id }-${ attribute.name }`;
- const updateAttributes = ( attributes: HydratedAttributeType[] ) => {
- setHydratedAttributes( attributes );
+ const handleChange = ( newAttributes: EnhancedProductAttribute[] ) => {
onChange(
- attributes.map( ( attr ) => {
+ newAttributes.map( ( attr ) => {
return {
...attr,
options: attr.terms
@@ -157,8 +80,8 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
recordEvent(
'product_remove_attribute_confirmation_confirm_click'
);
- updateAttributes(
- hydratedAttributes.filter(
+ handleChange(
+ value.filter(
( attr ) =>
fetchAttributeId( attr ) !==
fetchAttributeId( attribute )
@@ -169,9 +92,11 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
}
};
- const onAddNewAttributes = ( newAttributes: HydratedAttributeType[] ) => {
- updateAttributes( [
- ...( hydratedAttributes || [] ),
+ const onAddNewAttributes = (
+ newAttributes: EnhancedProductAttribute[]
+ ) => {
+ handleChange( [
+ ...( value || [] ),
...newAttributes
.filter(
( newAttr ) =>
@@ -193,18 +118,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
setShowAddAttributeModal( false );
};
- const filteredAttributes = value
- ? value.filter(
- ( attribute: ProductAttribute ) =>
- attribute.variation === isOnlyForVariations
- )
- : false;
-
- if (
- ! filteredAttributes ||
- filteredAttributes.length === 0 ||
- hydratedAttributes.length === 0
- ) {
+ if ( ! value.length ) {
return (
<>
= ( {
setShowAddAttributeModal( false );
} }
onAdd={ onAddNewAttributes }
- selectedAttributeIds={ ( filteredAttributes || [] ).map(
- ( attr ) => attr.id
- ) }
+ selectedAttributeIds={ [] }
/>
) }
@@ -242,9 +154,8 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
);
}
- const sortedAttributes = filteredAttributes.sort(
- ( a, b ) => a.position - b.position
- );
+ const sortedAttributes = value.sort( ( a, b ) => a.position - b.position );
+
const attributeKeyValues = value.reduce(
(
keyValue: Record< number | string, ProductAttribute >,
@@ -256,9 +167,9 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
{} as Record< number | string, ProductAttribute >
);
- const attribute = hydratedAttributes.find(
+ const editingAttribute = value.find(
( attr ) => fetchAttributeId( attr ) === editingAttributeId
- ) as HydratedAttributeType;
+ ) as EnhancedProductAttribute;
const editAttributeCopy = isOnlyForVariations
? __(
@@ -332,15 +243,13 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
/>
) }
- { editingAttributeId && (
+ { editingAttribute && (
= ( {
} ) }
onCancel={ () => setEditingAttributeId( null ) }
onEdit={ ( changedAttribute ) => {
- const newAttributesSet = [ ...hydratedAttributes ];
+ const newAttributesSet = [ ...value ];
const changedAttributeIndex: number =
newAttributesSet.findIndex( ( attr ) =>
attr.id !== 0
@@ -373,10 +282,10 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
changedAttribute
);
- updateAttributes( newAttributesSet );
+ handleChange( newAttributesSet );
setEditingAttributeId( null );
} }
- attribute={ attribute }
+ attribute={ editingAttribute }
/>
) }
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss b/plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-field.scss
similarity index 100%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/attribute-field.scss
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/attribute-field.scss
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.scss
similarity index 100%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.scss
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.scss
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx
similarity index 87%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx
index deaf03ee225..53da1cec567 100644
--- a/plugins/woocommerce-admin/client/products/fields/attribute-field/edit-attribute-modal.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/edit-attribute-modal.tsx
@@ -18,7 +18,7 @@ import {
AttributeTermInputField,
CustomAttributeTermInputField,
} from '../attribute-term-input-field';
-import { HydratedAttributeType } from './attribute-field';
+import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
import './edit-attribute-modal.scss';
@@ -36,8 +36,8 @@ type EditAttributeModalProps = {
updateAccessibleLabel?: string;
updateLabel?: string;
onCancel: () => void;
- onEdit: ( alteredAttribute: HydratedAttributeType ) => void;
- attribute: HydratedAttributeType;
+ onEdit: ( alteredAttribute: EnhancedProductAttribute ) => void;
+ attribute: EnhancedProductAttribute;
};
export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
@@ -64,7 +64,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
attribute,
} ) => {
const [ editableAttribute, setEditableAttribute ] = useState<
- HydratedAttributeType | undefined
+ EnhancedProductAttribute | undefined
>( { ...attribute } );
const isCustomAttribute = editableAttribute?.id === 0;
@@ -84,7 +84,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
}
onChange={ ( val ) =>
setEditableAttribute( {
- ...( editableAttribute as HydratedAttributeType ),
+ ...( editableAttribute as EnhancedProductAttribute ),
name: val,
} )
}
@@ -102,7 +102,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
attributeId={ editableAttribute?.id }
onChange={ ( val ) => {
setEditableAttribute( {
- ...( editableAttribute as HydratedAttributeType ),
+ ...( editableAttribute as EnhancedProductAttribute ),
terms: val,
} );
} }
@@ -115,7 +115,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
value={ editableAttribute?.options }
onChange={ ( val ) => {
setEditableAttribute( {
- ...( editableAttribute as HydratedAttributeType ),
+ ...( editableAttribute as EnhancedProductAttribute ),
options: val,
} );
} }
@@ -126,7 +126,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
setEditableAttribute( {
- ...( editableAttribute as HydratedAttributeType ),
+ ...( editableAttribute as EnhancedProductAttribute ),
visible: val,
} )
}
@@ -148,7 +148,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
isPrimary
label={ updateAccessibleLabel }
onClick={ () => {
- onEdit( editableAttribute as HydratedAttributeType );
+ onEdit( editableAttribute as EnhancedProductAttribute );
} }
>
{ updateLabel }
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-control/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-control/index.ts
new file mode 100644
index 00000000000..3220152ee6e
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/index.ts
@@ -0,0 +1 @@
+export * from './attribute-control';
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/test/add-attribute-modal.spec.tsx
similarity index 100%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/test/add-attribute-modal.spec.tsx
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/test/add-attribute-modal.spec.tsx
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-control/test/attribute-field.spec.tsx
similarity index 90%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/test/attribute-field.spec.tsx
index f4a85ee1c7c..8ba9808e619 100644
--- a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/attribute-field.spec.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/attribute-control/test/attribute-field.spec.tsx
@@ -8,7 +8,7 @@ import { ProductAttribute } from '@woocommerce/data';
/**
* Internal dependencies
*/
-import { AttributeField } from '../attribute-field';
+import { AttributeControl } from '../attribute-control';
const attributeList: ProductAttribute[] = [
{
@@ -102,7 +102,7 @@ jest.mock( '@woocommerce/components', () => ( {
},
} ) );
-describe( 'AttributeField', () => {
+describe( 'AttributeControl', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
@@ -110,17 +110,17 @@ describe( 'AttributeField', () => {
describe( 'empty state', () => {
it( 'should show subtitle and "Add first attribute" button', () => {
const { queryByText } = render(
- {} } />
+ {} } />
);
expect( queryByText( 'No attributes yet' ) ).toBeInTheDocument();
expect( queryByText( 'Add first attribute' ) ).toBeInTheDocument();
} );
} );
- it( 'should render the list of existing attributes', async () => {
+ it( 'should render the list of all attributes', async () => {
act( () => {
render(
- {} }
/>
@@ -128,20 +128,20 @@ describe( 'AttributeField', () => {
} );
expect(
- await screen.findByText( 'No attributes yet' )
+ await screen.queryByText( 'No attributes yet' )
).not.toBeInTheDocument();
expect(
- await screen.findByText( attributeList[ 0 ].name )
+ await screen.queryByText( attributeList[ 0 ].name )
).toBeInTheDocument();
expect(
await screen.queryByText( attributeList[ 1 ].name )
- ).not.toBeInTheDocument();
+ ).toBeInTheDocument();
} );
it( 'should render the first two terms of each option, and show "+ n more" for the rest', async () => {
act( () => {
render(
- {} }
attributeType="for-variations"
@@ -149,9 +149,6 @@ describe( 'AttributeField', () => {
);
} );
- expect(
- await screen.queryByText( attributeList[ 0 ].options[ 0 ] )
- ).not.toBeInTheDocument();
expect(
await screen.findByText( attributeList[ 1 ].options[ 0 ] )
).toBeInTheDocument();
@@ -173,7 +170,7 @@ describe( 'AttributeField', () => {
jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false );
act( () => {
render(
- {} }
/>
@@ -191,7 +188,7 @@ describe( 'AttributeField', () => {
act( () => {
render(
-
@@ -211,7 +208,7 @@ describe( 'AttributeField', () => {
const onChange = jest.fn();
act( () => {
render(
-
@@ -232,7 +229,7 @@ describe( 'AttributeField', () => {
act( () => {
render(
-
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/test/utils.spec.ts b/plugins/woocommerce-admin/client/products/fields/attribute-control/test/utils.spec.ts
similarity index 100%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/test/utils.spec.ts
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/test/utils.spec.ts
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts b/plugins/woocommerce-admin/client/products/fields/attribute-control/utils.ts
similarity index 100%
rename from plugins/woocommerce-admin/client/products/fields/attribute-field/utils.ts
rename to plugins/woocommerce-admin/client/products/fields/attribute-control/utils.ts
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-field/index.ts b/plugins/woocommerce-admin/client/products/fields/attribute-field/index.ts
deleted file mode 100644
index 938913f453f..00000000000
--- a/plugins/woocommerce-admin/client/products/fields/attribute-field/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './attribute-field';
diff --git a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx
index 7f19b7cdff1..8d3eaf35402 100644
--- a/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/attribute-input-field/attribute-input-field.tsx
@@ -22,12 +22,12 @@ import {
* Internal dependencies
*/
import './attribute-input-field.scss';
-import { HydratedAttributeType } from '../attribute-field';
+import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >;
type AttributeInputFieldProps = {
- value?: HydratedAttributeType | null;
+ value?: EnhancedProductAttribute | null;
onChange: (
value?:
| Omit< ProductAttribute, 'position' | 'visible' | 'variation' >
diff --git a/plugins/woocommerce-admin/client/products/fields/attributes/attributes.tsx b/plugins/woocommerce-admin/client/products/fields/attributes/attributes.tsx
index 85f905ce06d..ff768ee26c4 100644
--- a/plugins/woocommerce-admin/client/products/fields/attributes/attributes.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/attributes/attributes.tsx
@@ -6,7 +6,8 @@ import { ProductAttribute } from '@woocommerce/data';
/**
* Internal dependencies
*/
-import { AttributeField } from '../attribute-field';
+import { AttributeControl } from '../attribute-control';
+import { useProductAttributes } from '~/products/hooks/use-product-attributes';
type AttributesProps = {
value: ProductAttribute[];
@@ -19,12 +20,17 @@ export const Attributes: React.FC< AttributesProps > = ( {
onChange,
productId,
} ) => {
+ const { attributes, handleChange } = useProductAttributes( {
+ allAttributes: value,
+ onChange,
+ productId,
+ } );
+
return (
-
);
};
diff --git a/plugins/woocommerce-admin/client/products/fields/options/options.tsx b/plugins/woocommerce-admin/client/products/fields/options/options.tsx
index c35fa4e2695..bff9c0ff16a 100644
--- a/plugins/woocommerce-admin/client/products/fields/options/options.tsx
+++ b/plugins/woocommerce-admin/client/products/fields/options/options.tsx
@@ -7,7 +7,8 @@ import { useFormContext } from '@woocommerce/components';
/**
* Internal dependencies
*/
-import { AttributeField } from '../attribute-field';
+import { AttributeControl } from '../attribute-control';
+import { useProductAttributes } from '~/products/hooks/use-product-attributes';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
type OptionsProps = {
@@ -24,17 +25,24 @@ export const Options: React.FC< OptionsProps > = ( {
const { values } = useFormContext< Product >();
const { generateProductVariations } = useProductVariationsHelper();
- const handleChange = async ( attributes: ProductAttribute[] ) => {
- onChange( attributes );
- generateProductVariations( { ...values, attributes } );
- };
+ const { attributes, handleChange } = useProductAttributes( {
+ allAttributes: value,
+ isVariationAttributes: true,
+ onChange: ( newAttributes ) => {
+ onChange( newAttributes );
+ generateProductVariations( {
+ ...values,
+ attributes: newAttributes,
+ } );
+ },
+ productId,
+ } );
return (
-
);
};
diff --git a/plugins/woocommerce-admin/client/products/hooks/use-product-attributes.ts b/plugins/woocommerce-admin/client/products/hooks/use-product-attributes.ts
new file mode 100644
index 00000000000..f05dc993994
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/hooks/use-product-attributes.ts
@@ -0,0 +1,111 @@
+/**
+ * External dependencies
+ */
+import {
+ EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
+ ProductAttribute,
+ ProductAttributeTerm,
+} from '@woocommerce/data';
+import { resolveSelect } from '@wordpress/data';
+import { useCallback, useEffect, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { sift } from '../../utils';
+
+type useProductAttributesProps = {
+ allAttributes: ProductAttribute[];
+ isVariationAttributes?: boolean;
+ onChange: ( attributes: ProductAttribute[] ) => void;
+ productId?: number;
+};
+
+export type EnhancedProductAttribute = ProductAttribute & {
+ terms?: ProductAttributeTerm[];
+ visible?: boolean;
+};
+
+export function useProductAttributes( {
+ allAttributes = [],
+ isVariationAttributes = false,
+ onChange,
+ productId,
+}: useProductAttributesProps ) {
+ const getFilteredAttributes = () => {
+ return isVariationAttributes
+ ? allAttributes.filter( ( attribute ) => !! attribute.variation )
+ : allAttributes.filter( ( attribute ) => ! attribute.variation );
+ };
+
+ const [ attributes, setAttributes ] = useState<
+ EnhancedProductAttribute[]
+ >( getFilteredAttributes() );
+ const [ localAttributes, globalAttributes ]: ProductAttribute[][] = sift(
+ attributes,
+ ( attr: ProductAttribute ) => attr.id === 0
+ );
+
+ const fetchTerms = useCallback(
+ ( attributeId: number ) => {
+ return resolveSelect(
+ EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
+ )
+ .getProductAttributeTerms< ProductAttributeTerm[] >( {
+ attribute_id: attributeId,
+ product: productId,
+ } )
+ .then(
+ ( attributeTerms ) => {
+ return attributeTerms;
+ },
+ ( error ) => {
+ return error;
+ }
+ );
+ },
+ [ productId ]
+ );
+
+ const enhanceAttribute = (
+ globalAttribute: ProductAttribute,
+ terms: ProductAttributeTerm[]
+ ) => {
+ return {
+ ...globalAttribute,
+ terms: terms.length > 0 ? terms : undefined,
+ options: terms.length === 0 ? globalAttribute.options : [],
+ };
+ };
+
+ const handleChange = ( newAttributes: ProductAttribute[] ) => {
+ const otherAttributes = isVariationAttributes
+ ? allAttributes.filter( ( attribute ) => ! attribute.variation )
+ : allAttributes.filter( ( attribute ) => !! attribute.variation );
+ setAttributes( newAttributes );
+ onChange( [ ...otherAttributes, ...newAttributes ] );
+ };
+
+ useEffect( () => {
+ if ( ! getFilteredAttributes().length || attributes.length ) {
+ return;
+ }
+
+ Promise.all(
+ globalAttributes.map( ( attr ) => fetchTerms( attr.id ) )
+ ).then( ( termData ) => {
+ setAttributes( [
+ ...globalAttributes.map( ( attr, index ) =>
+ enhanceAttribute( attr, termData[ index ] )
+ ),
+ ...localAttributes,
+ ] );
+ } );
+ }, [ allAttributes, attributes, fetchTerms ] );
+
+ return {
+ attributes,
+ handleChange,
+ setAttributes,
+ };
+}
diff --git a/plugins/woocommerce/changelog/update-36304 b/plugins/woocommerce/changelog/update-36304
new file mode 100644
index 00000000000..695487b46d0
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-36304
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Move product attribute fetching logic into a separate hook
From d9daad3e9c28272421707940a72f0571f5502855 Mon Sep 17 00:00:00 2001
From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com>
Date: Tue, 17 Jan 2023 01:04:58 -0800
Subject: [PATCH 16/58] Converting product details section to utilize slot
fills (#36368)
* Working prototype of product details via slotfill
* Raising default order for product slot-fills
* Move logic to details field name component
* Adding order prop to all fills.
* Adding components changelog
* Adding changelog
* Deleting obsolete product details section files
* Reducing spacing between slot filled fields
---
.../update-36016-product-details-slotfill | 4 +
packages/js/components/src/index.ts | 4 +
.../src/product-section-layout/index.ts | 2 +
.../product-field-section.tsx | 39 +++
.../product-section-layout.tsx | 35 +++
.../src/product-section-layout/style.scss | 52 ++++
packages/js/components/src/style.scss | 1 +
.../woo-product-field-item.tsx | 2 +-
.../woo-product-section-item.tsx | 2 +-
.../fills/details-section/constants.ts | 2 +
.../details-field-categories.tsx | 25 ++
.../details-field-description.tsx | 36 +++
.../details-section/details-field-feature.tsx | 65 +++++
.../details-section/details-field-name.tsx | 104 ++++++++
.../details-section/details-field-summary.tsx | 36 +++
.../details-section/details-section-fills.tsx | 90 +++++++
.../products/fills/details-section/index.ts | 6 +
.../product-details-section.scss | 0
.../client/products/fills/index.ts | 1 +
.../layout/product-section-layout.scss | 2 +-
.../client/products/product-form.tsx | 9 +-
.../sections/product-details-section.tsx | 250 ------------------
.../test/product-details-section.spec.tsx | 89 -------
.../update-36016-product-details-slotfill | 4 +
24 files changed, 515 insertions(+), 345 deletions(-)
create mode 100644 packages/js/components/changelog/update-36016-product-details-slotfill
create mode 100644 packages/js/components/src/product-section-layout/index.ts
create mode 100644 packages/js/components/src/product-section-layout/product-field-section.tsx
create mode 100644 packages/js/components/src/product-section-layout/product-section-layout.tsx
create mode 100644 packages/js/components/src/product-section-layout/style.scss
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/constants.ts
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/details-field-categories.tsx
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/details-field-description.tsx
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/details-field-summary.tsx
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx
create mode 100644 plugins/woocommerce-admin/client/products/fills/details-section/index.ts
rename plugins/woocommerce-admin/client/products/{sections => fills/details-section}/product-details-section.scss (100%)
create mode 100644 plugins/woocommerce-admin/client/products/fills/index.ts
delete mode 100644 plugins/woocommerce-admin/client/products/sections/product-details-section.tsx
delete mode 100644 plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx
create mode 100644 plugins/woocommerce/changelog/update-36016-product-details-slotfill
diff --git a/packages/js/components/changelog/update-36016-product-details-slotfill b/packages/js/components/changelog/update-36016-product-details-slotfill
new file mode 100644
index 00000000000..d03cd2109ac
--- /dev/null
+++ b/packages/js/components/changelog/update-36016-product-details-slotfill
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Adding ProductSectionLayout component and changing default order for WooProductSectionItem component.
diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts
index 3430cb01281..94562eb2dce 100644
--- a/packages/js/components/src/index.ts
+++ b/packages/js/components/src/index.ts
@@ -87,4 +87,8 @@ export { CollapsibleContent } from './collapsible-content';
export { createOrderedChildren, sortFillsByOrder } from './utils';
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';
+export {
+ ProductSectionLayout as __experimentalProductSectionLayout,
+ ProductFieldSection as __experimentalProductFieldSection,
+} from './product-section-layout';
export * from './product-fields';
diff --git a/packages/js/components/src/product-section-layout/index.ts b/packages/js/components/src/product-section-layout/index.ts
new file mode 100644
index 00000000000..9c670115867
--- /dev/null
+++ b/packages/js/components/src/product-section-layout/index.ts
@@ -0,0 +1,2 @@
+export * from './product-section-layout';
+export * from './product-field-section';
diff --git a/packages/js/components/src/product-section-layout/product-field-section.tsx b/packages/js/components/src/product-section-layout/product-field-section.tsx
new file mode 100644
index 00000000000..5ae1d11b36c
--- /dev/null
+++ b/packages/js/components/src/product-section-layout/product-field-section.tsx
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { createElement } from '@wordpress/element';
+import { Card, CardBody } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { ProductSectionLayout } from './product-section-layout';
+import { WooProductFieldItem } from '../woo-product-field-item';
+
+type ProductFieldSectionProps = {
+ id: string;
+ title: string;
+ description: string | JSX.Element;
+ className?: string;
+};
+
+export const ProductFieldSection: React.FC< ProductFieldSectionProps > = ( {
+ id,
+ title,
+ description,
+ className,
+ children,
+} ) => (
+
+
+
+ { children }
+
+
+
+
+);
diff --git a/packages/js/components/src/product-section-layout/product-section-layout.tsx b/packages/js/components/src/product-section-layout/product-section-layout.tsx
new file mode 100644
index 00000000000..5c80ab9e426
--- /dev/null
+++ b/packages/js/components/src/product-section-layout/product-section-layout.tsx
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+import { Children, isValidElement, createElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { FormSection } from '../form-section';
+
+type ProductSectionLayoutProps = {
+ title: string;
+ description: string | JSX.Element;
+ className?: string;
+};
+
+export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( {
+ title,
+ description,
+ className,
+ children,
+} ) => (
+
+ { Children.map( children, ( child ) => {
+ if ( isValidElement( child ) && child.props.onChange ) {
+ return { child }
;
+ }
+ return child;
+ } ) }
+
+);
diff --git a/packages/js/components/src/product-section-layout/style.scss b/packages/js/components/src/product-section-layout/style.scss
new file mode 100644
index 00000000000..acd31a35894
--- /dev/null
+++ b/packages/js/components/src/product-section-layout/style.scss
@@ -0,0 +1,52 @@
+.woocommerce-form-section {
+ a {
+ text-decoration: none;
+ }
+
+ &__content {
+ .components-card {
+ border: 1px solid $gray-400;
+ border-radius: 2px;
+ box-shadow: none;
+ &__body {
+ padding: $gap-large;
+
+ > .components-base-control,
+ > .components-dropdown,
+ > .woocommerce-rich-text-editor {
+ &:not(:first-child):not(.components-radio-control) {
+ margin-top: $gap-large - $gap-smaller;
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+
+ .woocommerce-product-form__field:not(:first-child) {
+ margin-top: $gap-large;
+
+ > .components-base-control {
+ margin-bottom: 0;
+ }
+ }
+
+ .components-radio-control .components-v-stack {
+ gap: $gap-small;
+ }
+
+ .woocommerce-collapsible-content {
+ margin-top: $gap-large;
+ }
+ }
+
+ &__header {
+ p > span {
+ display: block;
+ margin-bottom: $gap-smaller;
+ }
+ }
+
+ &:not(:first-child) {
+ margin-top: $gap-largest;
+ }
+}
diff --git a/packages/js/components/src/style.scss b/packages/js/components/src/style.scss
index 6ee55cde07f..cf6478c655a 100644
--- a/packages/js/components/src/style.scss
+++ b/packages/js/components/src/style.scss
@@ -55,3 +55,4 @@
@import 'tour-kit/style.scss';
@import 'collapsible-content/style.scss';
@import 'form/style.scss';
+@import 'product-section-layout/style.scss';
diff --git a/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx b/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx
index 93bbf4c066d..3a62adf24df 100644
--- a/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx
+++ b/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx
@@ -23,7 +23,7 @@ type WooProductFieldSlotProps = {
export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
-} = ( { children, order = 1, section } ) => (
+} = ( { children, order = 20, section } ) => (
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren< Fill.Props >(
diff --git a/packages/js/components/src/woo-product-section-item/woo-product-section-item.tsx b/packages/js/components/src/woo-product-section-item/woo-product-section-item.tsx
index ead5114a62c..5a79e5fa075 100644
--- a/packages/js/components/src/woo-product-section-item/woo-product-section-item.tsx
+++ b/packages/js/components/src/woo-product-section-item/woo-product-section-item.tsx
@@ -23,7 +23,7 @@ type WooProductFieldSlotProps = {
export const WooProductSectionItem: React.FC< WooProductSectionItemProps > & {
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
-} = ( { children, order = 1, location } ) => (
+} = ( { children, order = 20, location } ) => (
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren< Fill.Props >(
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/constants.ts b/plugins/woocommerce-admin/client/products/fills/details-section/constants.ts
new file mode 100644
index 00000000000..4404df12093
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/constants.ts
@@ -0,0 +1,2 @@
+export const PRODUCT_DETAILS_SLUG = 'product-details';
+export const DETAILS_SECTION_ID = 'general/details';
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-categories.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-categories.tsx
new file mode 100644
index 00000000000..87e080ba5e3
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-categories.tsx
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { useFormContext } from '@woocommerce/components';
+import { Product, ProductCategory } from '@woocommerce/data';
+
+/**
+ * Internal dependencies
+ */
+import { CategoryField } from '../../fields/category-field';
+
+export const DetailsCategoriesField = () => {
+ const { getInputProps } = useFormContext< Product >();
+
+ return (
+ [] >(
+ 'categories'
+ ) }
+ />
+ );
+};
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-description.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-description.tsx
new file mode 100644
index 00000000000..36f445ab3c0
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-description.tsx
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ useFormContext,
+ __experimentalRichTextEditor as RichTextEditor,
+} from '@woocommerce/components';
+import { Product } from '@woocommerce/data';
+import { BlockInstance, serialize, parse } from '@wordpress/blocks';
+import { useState } from '@wordpress/element';
+
+export const DetailsDescriptionField = () => {
+ const { setValue, values } = useFormContext< Product >();
+ const [ descriptionBlocks, setDescriptionBlocks ] = useState<
+ BlockInstance[]
+ >( parse( values.description || '' ) );
+
+ return (
+ {
+ setDescriptionBlocks( blocks );
+ if ( ! descriptionBlocks.length ) {
+ return;
+ }
+ setValue( 'description', serialize( blocks ) );
+ } }
+ placeholder={ __(
+ 'Describe this product. What makes it unique? What are its most important features?',
+ 'woocommerce'
+ ) }
+ />
+ );
+};
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx
new file mode 100644
index 00000000000..350871b4c03
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-feature.tsx
@@ -0,0 +1,65 @@
+/**
+ * External dependencies
+ */
+import { CheckboxControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import {
+ useFormContext,
+ Link,
+ __experimentalTooltip as Tooltip,
+} from '@woocommerce/components';
+import interpolateComponents from '@automattic/interpolate-components';
+import { Product } from '@woocommerce/data';
+import { recordEvent } from '@woocommerce/tracks';
+
+/**
+ * Internal dependencies
+ */
+import { getCheckboxTracks } from '../../sections/utils';
+import { PRODUCT_DETAILS_SLUG } from './index';
+
+export const DetailsFeatureField = () => {
+ const { getCheckboxControlProps } = useFormContext< Product >();
+
+ return (
+
+ { __( 'Feature this product', 'woocommerce' ) }
+
+ recordEvent(
+ 'add_product_learn_more',
+ {
+ category:
+ PRODUCT_DETAILS_SLUG,
+ }
+ )
+ }
+ >
+ { __( 'Learn more', 'woocommerce' ) }
+
+ ),
+ },
+ } ) }
+ />
+ >
+ }
+ { ...getCheckboxControlProps(
+ 'featured',
+ getCheckboxTracks( 'featured' )
+ ) }
+ />
+ );
+};
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx
new file mode 100644
index 00000000000..c8513e6c755
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-name.tsx
@@ -0,0 +1,104 @@
+/**
+ * External dependencies
+ */
+import { Button, TextControl } from '@wordpress/components';
+import { useSelect } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { cleanForSlug } from '@wordpress/url';
+import { useFormContext } from '@woocommerce/components';
+import interpolateComponents from '@automattic/interpolate-components';
+import {
+ Product,
+ PRODUCTS_STORE_NAME,
+ WCDataSelector,
+} from '@woocommerce/data';
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { EditProductLinkModal } from '../../shared/edit-product-link-modal';
+import { PRODUCT_DETAILS_SLUG } from './index';
+
+export const DetailsNameField = ( {} ) => {
+ const [ showProductLinkEditModal, setShowProductLinkEditModal ] =
+ useState( false );
+ const { getInputProps, values, touched, errors, setValue } =
+ useFormContext< Product >();
+
+ const { permalinkPrefix, permalinkSuffix } = useSelect(
+ ( select: WCDataSelector ) => {
+ const { getPermalinkParts } = select( PRODUCTS_STORE_NAME );
+ if ( values.id ) {
+ const parts = getPermalinkParts( values.id );
+ return {
+ permalinkPrefix: parts?.prefix,
+ permalinkSuffix: parts?.suffix,
+ };
+ }
+ return {};
+ }
+ );
+
+ const hasNameError = () => {
+ return Boolean( touched.name ) && Boolean( errors.name );
+ };
+
+ const setSkuIfEmpty = () => {
+ if ( values.sku || ! values.name?.length ) {
+ return;
+ }
+ setValue( 'sku', cleanForSlug( values.name ) );
+ };
+ return (
+
+
+ { __( '(required)', 'woocommerce' ) }
+
+ ),
+ },
+ } ) }
+ name={ `${ PRODUCT_DETAILS_SLUG }-name` }
+ placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) }
+ { ...getInputProps( 'name', {
+ onBlur: setSkuIfEmpty,
+ } ) }
+ />
+ { values.id && ! hasNameError() && permalinkPrefix && (
+
+ { __( 'Product link', 'woocommerce' ) }
+ :
+
+ { permalinkPrefix }
+ { values.slug || cleanForSlug( values.name ) }
+ { permalinkSuffix }
+
+ setShowProductLinkEditModal( true ) }
+ >
+ { __( 'Edit', 'woocommerce' ) }
+
+
+ ) }
+ { showProductLinkEditModal && (
+ setShowProductLinkEditModal( false ) }
+ onSaved={ () => setShowProductLinkEditModal( false ) }
+ />
+ ) }
+
+ );
+};
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-field-summary.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-summary.tsx
new file mode 100644
index 00000000000..aa1304e5d36
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-field-summary.tsx
@@ -0,0 +1,36 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ useFormContext,
+ __experimentalRichTextEditor as RichTextEditor,
+} from '@woocommerce/components';
+import { Product } from '@woocommerce/data';
+import { BlockInstance, serialize, parse } from '@wordpress/blocks';
+import { useState } from '@wordpress/element';
+
+export const DetailsSummaryField = () => {
+ const { setValue, values } = useFormContext< Product >();
+ const [ summaryBlocks, setSummaryBlocks ] = useState< BlockInstance[] >(
+ parse( values.short_description || '' )
+ );
+
+ return (
+ {
+ setSummaryBlocks( blocks );
+ if ( ! summaryBlocks.length ) {
+ return;
+ }
+ setValue( 'short_description', serialize( blocks ) );
+ } }
+ placeholder={ __(
+ "Summarize this product in 1-2 short sentences. We'll show it at the top of the page.",
+ 'woocommerce'
+ ) }
+ />
+ );
+};
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx
new file mode 100644
index 00000000000..dae3909e352
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ __experimentalWooProductSectionItem as WooProductSectionItem,
+ __experimentalWooProductFieldItem as WooProductFieldItem,
+ __experimentalProductFieldSection as ProductFieldSection,
+} from '@woocommerce/components';
+import { registerPlugin } from '@wordpress/plugins';
+
+/**
+ * Internal dependencies
+ */
+import {
+ DetailsNameField,
+ DetailsCategoriesField,
+ DetailsFeatureField,
+ DetailsSummaryField,
+ DetailsDescriptionField,
+ DETAILS_SECTION_ID,
+} from './index';
+import './product-details-section.scss';
+
+const DetailsSection = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+);
+
+registerPlugin( 'wc-admin-product-editor-details-section', {
+ // @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
+ scope: 'woocommerce-product-editor',
+ render: () => {
+ return ;
+ },
+} );
diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/index.ts b/plugins/woocommerce-admin/client/products/fills/details-section/index.ts
new file mode 100644
index 00000000000..ece2335fc8d
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/details-section/index.ts
@@ -0,0 +1,6 @@
+export * from './details-field-name';
+export * from './details-field-categories';
+export * from './details-field-feature';
+export * from './details-field-summary';
+export * from './details-field-description';
+export * from './constants';
diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.scss b/plugins/woocommerce-admin/client/products/fills/details-section/product-details-section.scss
similarity index 100%
rename from plugins/woocommerce-admin/client/products/sections/product-details-section.scss
rename to plugins/woocommerce-admin/client/products/fills/details-section/product-details-section.scss
diff --git a/plugins/woocommerce-admin/client/products/fills/index.ts b/plugins/woocommerce-admin/client/products/fills/index.ts
new file mode 100644
index 00000000000..3bdc560ed80
--- /dev/null
+++ b/plugins/woocommerce-admin/client/products/fills/index.ts
@@ -0,0 +1 @@
+export * from './details-section/details-section-fills';
diff --git a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss
index acd31a35894..83d72682e05 100644
--- a/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss
+++ b/plugins/woocommerce-admin/client/products/layout/product-section-layout.scss
@@ -23,7 +23,7 @@
}
.woocommerce-product-form__field:not(:first-child) {
- margin-top: $gap-large;
+ margin-top: $gap-large - $gap-smaller;
> .components-base-control {
margin-bottom: 0;
diff --git a/plugins/woocommerce-admin/client/products/product-form.tsx b/plugins/woocommerce-admin/client/products/product-form.tsx
index 6be4974c9e8..655e10006d2 100644
--- a/plugins/woocommerce-admin/client/products/product-form.tsx
+++ b/plugins/woocommerce-admin/client/products/product-form.tsx
@@ -8,13 +8,13 @@ import {
} from '@woocommerce/components';
import { PartialProduct, Product } from '@woocommerce/data';
import { Ref } from 'react';
+import { PluginArea } from '@wordpress/plugins';
/**
* Internal dependencies
*/
import { ProductFormHeader } from './layout/product-form-header';
import { ProductFormLayout } from './layout/product-form-layout';
-import { ProductDetailsSection } from './sections/product-details-section';
import { ProductInventorySection } from './sections/product-inventory-section';
import { PricingSection } from './sections/pricing-section';
import { ProductShippingSection } from './sections/product-shipping-section';
@@ -26,6 +26,8 @@ import { OptionsSection } from './sections/options-section';
import { ProductFormFooter } from './layout/product-form-footer';
import { ProductFormTab } from './product-form-tab';
+import './fills';
+
export const ProductForm: React.FC< {
product?: PartialProduct;
formRef?: Ref< FormRef< Partial< Product > > >;
@@ -48,10 +50,9 @@ export const ProductForm: React.FC< {
-
+
-
+ { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
+
);
};
diff --git a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx b/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx
deleted file mode 100644
index db2abe0848f..00000000000
--- a/plugins/woocommerce-admin/client/products/sections/product-details-section.tsx
+++ /dev/null
@@ -1,250 +0,0 @@
-/**
- * External dependencies
- */
-import {
- CheckboxControl,
- Button,
- TextControl,
- Card,
- CardBody,
-} from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
-import { useState } from '@wordpress/element';
-import { cleanForSlug } from '@wordpress/url';
-import {
- Link,
- useFormContext,
- __experimentalRichTextEditor as RichTextEditor,
- __experimentalTooltip as Tooltip,
- __experimentalWooProductFieldItem as WooProductFieldItem,
-} from '@woocommerce/components';
-import interpolateComponents from '@automattic/interpolate-components';
-import {
- Product,
- ProductCategory,
- PRODUCTS_STORE_NAME,
- WCDataSelector,
-} from '@woocommerce/data';
-import { recordEvent } from '@woocommerce/tracks';
-import { BlockInstance, serialize, parse } from '@wordpress/blocks';
-
-/**
- * Internal dependencies
- */
-import './product-details-section.scss';
-import { CategoryField } from '../fields/category-field';
-import { EditProductLinkModal } from '../shared/edit-product-link-modal';
-import { getCheckboxTracks } from './utils';
-import { ProductSectionLayout } from '../layout/product-section-layout';
-
-const PRODUCT_DETAILS_SLUG = 'product-details';
-
-export const ProductDetailsSection: React.FC = () => {
- const {
- getCheckboxControlProps,
- getInputProps,
- values,
- touched,
- errors,
- setValue,
- } = useFormContext< Product >();
- const [ showProductLinkEditModal, setShowProductLinkEditModal ] =
- useState( false );
- const [ descriptionBlocks, setDescriptionBlocks ] = useState<
- BlockInstance[]
- >( parse( values.description || '' ) );
- const [ summaryBlocks, setSummaryBlocks ] = useState< BlockInstance[] >(
- parse( values.short_description || '' )
- );
- const { permalinkPrefix, permalinkSuffix } = useSelect(
- ( select: WCDataSelector ) => {
- const { getPermalinkParts } = select( PRODUCTS_STORE_NAME );
- if ( values.id ) {
- const parts = getPermalinkParts( values.id );
- return {
- permalinkPrefix: parts?.prefix,
- permalinkSuffix: parts?.suffix,
- };
- }
- return {};
- }
- );
-
- const hasNameError = () => {
- return Boolean( touched.name ) && Boolean( errors.name );
- };
-
- const setSkuIfEmpty = () => {
- if ( values.sku || ! values.name?.length ) {
- return;
- }
- setValue( 'sku', cleanForSlug( values.name ) );
- };
-
- return (
-
-
-
-
-
- { __(
- '(required)',
- 'woocommerce'
- ) }
-
- ),
- },
- } ) }
- name={ `${ PRODUCT_DETAILS_SLUG }-name` }
- placeholder={ __(
- 'e.g. 12 oz Coffee Mug',
- 'woocommerce'
- ) }
- { ...getInputProps( 'name', {
- onBlur: setSkuIfEmpty,
- } ) }
- />
- { values.id && ! hasNameError() && permalinkPrefix && (
-
- { __( 'Product link', 'woocommerce' ) }
- :
-
- { permalinkPrefix }
- { values.slug ||
- cleanForSlug( values.name ) }
- { permalinkSuffix }
-
-
- setShowProductLinkEditModal( true )
- }
- >
- { __( 'Edit', 'woocommerce' ) }
-
-
- ) }
-
- []
- >( 'categories' ) }
- />
-
- { __( 'Feature this product', 'woocommerce' ) }
-
- recordEvent(
- 'add_product_learn_more',
- {
- category:
- PRODUCT_DETAILS_SLUG,
- }
- )
- }
- >
- { __(
- 'Learn more',
- 'woocommerce'
- ) }
-
- ),
- },
- } ) }
- />
- >
- }
- { ...getCheckboxControlProps(
- 'featured',
- getCheckboxTracks( 'featured' )
- ) }
- />
- { showProductLinkEditModal && (
-
- setShowProductLinkEditModal( false )
- }
- onSaved={ () =>
- setShowProductLinkEditModal( false )
- }
- />
- ) }
- {
- setSummaryBlocks( blocks );
- if ( ! summaryBlocks.length ) {
- return;
- }
- setValue(
- 'short_description',
- serialize( blocks )
- );
- } }
- placeholder={ __(
- "Summarize this product in 1-2 short sentences. We'll show it at the top of the page.",
- 'woocommerce'
- ) }
- />
- {
- setDescriptionBlocks( blocks );
- if ( ! descriptionBlocks.length ) {
- return;
- }
- setValue( 'description', serialize( blocks ) );
- } }
- placeholder={ __(
- 'Describe this product. What makes it unique? What are its most important features?',
- 'woocommerce'
- ) }
- />
-
-
-
-
- );
-};
diff --git a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx b/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx
deleted file mode 100644
index fed5d6a532a..00000000000
--- a/plugins/woocommerce-admin/client/products/sections/test/product-details-section.spec.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * External dependencies
- */
-import { createRegistry, RegistryProvider, useSelect } from '@wordpress/data';
-import { Form } from '@woocommerce/components';
-import { Product } from '@woocommerce/data';
-import { render, screen } from '@testing-library/react';
-// 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 { store as blockEditorStore } 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 { store as coreDataStore } from '@wordpress/core-data';
-// eslint-disable-next-line @woocommerce/dependency-group
-import userEvent from '@testing-library/user-event';
-
-/**
- * Internal dependencies
- */
-import { ProductDetailsSection } from '../product-details-section';
-import { validate } from '../../product-validation';
-
-jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
-jest.mock( '@wordpress/data', () => ( {
- ...jest.requireActual( '@wordpress/data' ),
- useSelect: jest.fn(),
-} ) );
-
-const registry = createRegistry();
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore No types for this exist yet.
-registry.register( coreDataStore );
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore No types for this exist yet.
-registry.register( blockEditorStore );
-
-describe( 'ProductDetailsSection', () => {
- const useSelectMock = useSelect as jest.Mock;
-
- beforeEach( () => {
- jest.clearAllMocks();
- } );
-
- describe( 'when editing a product', () => {
- const product: Partial< Product > = {
- id: 1,
- name: 'Lorem',
- slug: 'lorem',
- };
- const permalinkPrefix = 'http://localhost/';
- const linkUrl = permalinkPrefix + product.slug;
-
- beforeEach( () => {
- useSelectMock.mockReturnValue( {
- permalinkPrefix,
- } );
- } );
-
- it( 'should render the product link', () => {
- render(
-
-
-
- );
-
- expect( screen.queryByText( linkUrl ) ).toBeInTheDocument();
- } );
-
- it( 'should hide the product link if field name has errors', () => {
- render(
-
-
-
- );
- userEvent.clear(
- screen.getByLabelText( 'Name', { exact: false } )
- );
- userEvent.tab();
-
- expect( screen.queryByText( linkUrl ) ).not.toBeInTheDocument();
- } );
- } );
-} );
diff --git a/plugins/woocommerce/changelog/update-36016-product-details-slotfill b/plugins/woocommerce/changelog/update-36016-product-details-slotfill
new file mode 100644
index 00000000000..aa1c65b371e
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-36016-product-details-slotfill
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Reimplementing product details fields in product editor as slot fills.
From 0f78e322167d7724c8a2594c60677c052596f7fb Mon Sep 17 00:00:00 2001
From: Matthias Kittsteiner
Date: Tue, 17 Jan 2023 10:39:10 +0100
Subject: [PATCH 17/58] Add context to countries shipping to prefix (#36254)
Closes https://github.com/woocommerce/woocommerce/issues/36242
---
.../changelog/countries-shipping-to-prefix-context | 4 ++++
plugins/woocommerce/includes/class-wc-countries.php | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 plugins/woocommerce/changelog/countries-shipping-to-prefix-context
diff --git a/plugins/woocommerce/changelog/countries-shipping-to-prefix-context b/plugins/woocommerce/changelog/countries-shipping-to-prefix-context
new file mode 100644
index 00000000000..2caa9243557
--- /dev/null
+++ b/plugins/woocommerce/changelog/countries-shipping-to-prefix-context
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Add context to countries shipping to prefix
diff --git a/plugins/woocommerce/includes/class-wc-countries.php b/plugins/woocommerce/includes/class-wc-countries.php
index a1a9aa8507f..bfaf83a6b06 100644
--- a/plugins/woocommerce/includes/class-wc-countries.php
+++ b/plugins/woocommerce/includes/class-wc-countries.php
@@ -410,7 +410,7 @@ class WC_Countries {
public function shipping_to_prefix( $country_code = '' ) {
$country_code = $country_code ? $country_code : WC()->customer->get_shipping_country();
$countries = array( 'AE', 'CZ', 'DO', 'GB', 'NL', 'PH', 'US', 'USAF' );
- $return = in_array( $country_code, $countries, true ) ? __( 'to the', 'woocommerce' ) : __( 'to', 'woocommerce' );
+ $return = in_array( $country_code, $countries, true ) ? _x( 'to the', 'shipping country prefix', 'woocommerce' ) : _x( 'to', 'shipping country prefix', 'woocommerce' );
return apply_filters( 'woocommerce_countries_shipping_to_prefix', $return, $country_code );
}
From 2bf9f577952d7a225365fe7728b3187b5be7b701 Mon Sep 17 00:00:00 2001
From: Barry Hughes <3594411+barryhughes@users.noreply.github.com>
Date: Tue, 17 Jan 2023 08:06:25 -0800
Subject: [PATCH 18/58] Restore pre-7.2.0 quantity selector behavior (#36460)
Prior to 7.2.0 the quantity input was hidden if input min and max were identical (either because the product was sold individually, or because of min/max products config). This change restores that behavior, but makes it possible to render the input in readonly mode if desired (via filters).
---
.../changelog/fix-36007-quantity-selector | 4 ++++
.../changelog/fix-36007-sold-individually | 4 ----
.../fix-36007-sold-individually-amendment | 5 -----
.../client/legacy/css/_common.scss | 8 --------
.../client/legacy/css/twenty-nineteen.scss | 1 -
.../client/legacy/css/twenty-seventeen.scss | 1 -
.../client/legacy/css/twenty-twenty-one.scss | 1 -
.../legacy/css/twenty-twenty-three.scss | 1 -
.../client/legacy/css/twenty-twenty-two.scss | 1 -
.../client/legacy/css/twenty-twenty.scss | 1 -
.../legacy/css/woocommerce-blocktheme.scss | 1 -
.../client/legacy/css/woocommerce.scss | 1 -
.../includes/wc-template-functions.php | 20 ++++++++++++++++++-
.../templates/global/quantity-input.php | 18 +++++++----------
14 files changed, 30 insertions(+), 37 deletions(-)
create mode 100644 plugins/woocommerce/changelog/fix-36007-quantity-selector
delete mode 100644 plugins/woocommerce/changelog/fix-36007-sold-individually
delete mode 100644 plugins/woocommerce/changelog/fix-36007-sold-individually-amendment
delete mode 100644 plugins/woocommerce/client/legacy/css/_common.scss
diff --git a/plugins/woocommerce/changelog/fix-36007-quantity-selector b/plugins/woocommerce/changelog/fix-36007-quantity-selector
new file mode 100644
index 00000000000..e3d2f700ea1
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-36007-quantity-selector
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore the pre-7.2.0 behavior for single product quantity inputs.
diff --git a/plugins/woocommerce/changelog/fix-36007-sold-individually b/plugins/woocommerce/changelog/fix-36007-sold-individually
deleted file mode 100644
index 727cdd0ccc2..00000000000
--- a/plugins/woocommerce/changelog/fix-36007-sold-individually
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: tweak
-
-By default, hide the quantity selector within the single product page if a product is sold individually.
diff --git a/plugins/woocommerce/changelog/fix-36007-sold-individually-amendment b/plugins/woocommerce/changelog/fix-36007-sold-individually-amendment
deleted file mode 100644
index 6b14c30ea53..00000000000
--- a/plugins/woocommerce/changelog/fix-36007-sold-individually-amendment
+++ /dev/null
@@ -1,5 +0,0 @@
-Significance: patch
-Type: tweak
-Comment: We're tweaking an unreleased change which is already covered by a changelog entry added in PR#36350.
-
-
diff --git a/plugins/woocommerce/client/legacy/css/_common.scss b/plugins/woocommerce/client/legacy/css/_common.scss
deleted file mode 100644
index 9737c39c898..00000000000
--- a/plugins/woocommerce/client/legacy/css/_common.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Contains rules common to all supported frontend themes.
- */
-
-/* We do not wish to display the quantity selector (within single product pages) if the product is sold individually. */
-.woocommerce.single-product .product.sold-individually .quantity {
- display: none;
-}
diff --git a/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss b/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss
index 41630ae0b43..fd257184f74 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-nineteen.scss
@@ -1,4 +1,3 @@
-@import "common";
@import 'mixins';
/**
diff --git a/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss b/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss
index 7e77f25c820..f2c0af5bb9f 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-seventeen.scss
@@ -1,7 +1,6 @@
/**
* Twenty Seventeen integration styles
*/
-@import "common";
@import "mixins";
@import "animation";
diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss
index e7bbab55a2f..199deae3fc8 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-one.scss
@@ -1,4 +1,3 @@
-@import "common";
@import "mixins";
/**
diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss
index 6c6a8fa48d9..0db0c019bc0 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-three.scss
@@ -25,7 +25,6 @@
font-style: normal;
}
-@import "common";
@import "mixins";
@import "animation";
diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss
index 7a54fe2318d..3105e01ce15 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-twenty-two.scss
@@ -25,7 +25,6 @@
font-style: normal;
}
-@import "common";
@import "mixins";
@import "animation";
@import "variables";
diff --git a/plugins/woocommerce/client/legacy/css/twenty-twenty.scss b/plugins/woocommerce/client/legacy/css/twenty-twenty.scss
index 0e758990c06..6f91033da6c 100644
--- a/plugins/woocommerce/client/legacy/css/twenty-twenty.scss
+++ b/plugins/woocommerce/client/legacy/css/twenty-twenty.scss
@@ -1,4 +1,3 @@
-@import "common";
@import "mixins";
/**
diff --git a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss
index 41b17b64088..1701887309a 100644
--- a/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss
+++ b/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss
@@ -2,7 +2,6 @@
* woocommerce-blocktheme.scss
* Block theme default styles to ensure WooCommerce looks better out of the box with block themes that are not optimised for WooCommerce specifically.
*/
-@import "common";
@import "fonts";
@import "variables";
diff --git a/plugins/woocommerce/client/legacy/css/woocommerce.scss b/plugins/woocommerce/client/legacy/css/woocommerce.scss
index 9b475942929..efa4f5432d6 100644
--- a/plugins/woocommerce/client/legacy/css/woocommerce.scss
+++ b/plugins/woocommerce/client/legacy/css/woocommerce.scss
@@ -7,7 +7,6 @@
/**
* Imports
*/
-@import "common";
@import "mixins";
@import "variables";
@import "animation";
diff --git a/plugins/woocommerce/includes/wc-template-functions.php b/plugins/woocommerce/includes/wc-template-functions.php
index 4b3ef3ec827..e07f77e4134 100644
--- a/plugins/woocommerce/includes/wc-template-functions.php
+++ b/plugins/woocommerce/includes/wc-template-functions.php
@@ -1797,6 +1797,7 @@ if ( ! function_exists( 'woocommerce_quantity_input' ) ) {
// When autocomplete is enabled in firefox, it will overwrite actual value with what user entered last. So we default to off.
// See @link https://github.com/woocommerce/woocommerce/issues/30733.
'autocomplete' => apply_filters( 'woocommerce_quantity_input_autocomplete', 'off', $product ),
+ 'readonly' => false,
);
$args = apply_filters( 'woocommerce_quantity_input_args', wp_parse_args( $args, $defaults ), $product );
@@ -1810,8 +1811,25 @@ if ( ! function_exists( 'woocommerce_quantity_input' ) ) {
$args['max_value'] = $args['min_value'];
}
- ob_start();
+ /**
+ * The input type attribute will generally be 'number' unless the quantity cannot be changed, in which case
+ * it will be set to 'hidden'. An exception is made for non-hidden readonly inputs: in this case we set the
+ * type to 'text' (this prevents most browsers from rendering increment/decrement arrows, which are useless
+ * and/or confusing in this context).
+ */
+ $type = $args['min_value'] > 0 && $args['min_value'] === $args['max_value'] ? 'hidden' : 'number';
+ $type = $args['readonly'] && 'hidden' !== $type ? 'text' : $type;
+ /**
+ * Controls the quantity input's type attribute.
+ *
+ * @since 7.4.0
+ *
+ * @param string $type A valid input type attribute value, usually 'number' or 'hidden'.
+ */
+ $args['type'] = apply_filters( 'woocommerce_quantity_input_type', $type );
+
+ ob_start();
wc_get_template( 'global/quantity-input.php', $args );
if ( $echo ) {
diff --git a/plugins/woocommerce/templates/global/quantity-input.php b/plugins/woocommerce/templates/global/quantity-input.php
index 5c346ecd307..07f865bf071 100644
--- a/plugins/woocommerce/templates/global/quantity-input.php
+++ b/plugins/woocommerce/templates/global/quantity-input.php
@@ -12,7 +12,10 @@
*
* @see https://docs.woocommerce.com/document/template-structure/
* @package WooCommerce\Templates
- * @version 7.2.1
+ * @version 7.4.0
+ *
+ * @var bool $readonly If the input should be set to readonly mode.
+ * @var string $type The input type attribute.
*/
defined( 'ABSPATH' ) || exit;
@@ -20,13 +23,6 @@ defined( 'ABSPATH' ) || exit;
/* translators: %s: Quantity. */
$label = ! empty( $args['product_name'] ) ? sprintf( esc_html__( '%s quantity', 'woocommerce' ), wp_strip_all_tags( $args['product_name'] ) ) : esc_html__( 'Quantity', 'woocommerce' );
-// In some cases we wish to display the quantity but not allow for it to be changed.
-if ( $max_value && $min_value === $max_value ) {
- $is_readonly = true;
- $input_value = $min_value;
-} else {
- $is_readonly = false;
-}
?>
+ type=""
+
id=""
class=""
name=""
@@ -49,7 +45,7 @@ if ( $max_value && $min_value === $max_value ) {
size="4"
min=""
max=""
-
+
step=""
placeholder=""
inputmode=""
From 1fb60677fcb6a75eb5ef3d96c1d354bffd4400b7 Mon Sep 17 00:00:00 2001
From: barryhughes <3594411+barryhughes@users.noreply.github.com>
Date: Tue, 17 Jan 2023 13:47:32 -0800
Subject: [PATCH 19/58] Changelog.
---
plugins/woocommerce/changelog/fix-i18n-states-comment-typo | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 plugins/woocommerce/changelog/fix-i18n-states-comment-typo
diff --git a/plugins/woocommerce/changelog/fix-i18n-states-comment-typo b/plugins/woocommerce/changelog/fix-i18n-states-comment-typo
new file mode 100644
index 00000000000..835d4d724bd
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-i18n-states-comment-typo
@@ -0,0 +1,4 @@
+Significance: patch
+Type: tweak
+
+Corrects a typo in the i18n/states.php file, relating to our list of Iranian states.
From 4bb137637f8d1a9f777705ea1dd9bd1739211ae0 Mon Sep 17 00:00:00 2001
From: barryhughes <3594411+barryhughes@users.noreply.github.com>
Date: Tue, 17 Jan 2023 13:59:36 -0800
Subject: [PATCH 20/58] Update API tests re revised email image header tooltip
text.
---
.../tests/api-core-tests/tests/settings/settings-crud.test.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js
index 78853b7e380..77edca5c84a 100644
--- a/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js
+++ b/plugins/woocommerce/tests/api-core-tests/tests/settings/settings-crud.test.js
@@ -1370,10 +1370,10 @@ test.describe('Settings API tests: CRUD', () => {
expect.objectContaining({
"id": "woocommerce_email_header_image",
"label": "Header image",
- "description": "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).",
+ "description": "Paste the URL of an image you want to show in the email header. Upload images using the media uploader (Media > Add New).",
"type": "text",
"default": "",
- "tip": "URL to an image you want to show in the email header. Upload images using the media uploader (Admin > Media).",
+ "tip": "Paste the URL of an image you want to show in the email header. Upload images using the media uploader (Media > Add New).",
"value": "",
})
]));
From c41eccc58ef65a00def17f6adaed946dc657425b Mon Sep 17 00:00:00 2001
From: barryhughes <3594411+barryhughes@users.noreply.github.com>
Date: Fri, 13 Jan 2023 14:38:56 -0800
Subject: [PATCH 21/58] Check a valid product object was obtained before
calling its methods.
---
.../changelog/fix-36325-download-permissions | 4 ++++
.../src/Internal/DownloadPermissionsAdjuster.php | 16 +++++++++++++++-
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 plugins/woocommerce/changelog/fix-36325-download-permissions
diff --git a/plugins/woocommerce/changelog/fix-36325-download-permissions b/plugins/woocommerce/changelog/fix-36325-download-permissions
new file mode 100644
index 00000000000..c8e65c7fc0d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-36325-download-permissions
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+When adjusting download permissions, confirm the child products have not been removed.
diff --git a/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php b/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
index a30c504e63e..b631f92e489 100644
--- a/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
+++ b/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
@@ -99,7 +99,21 @@ class DownloadPermissionsAdjuster {
$children_with_downloads = array();
foreach ( $children_ids as $child_id ) {
- $child = wc_get_product( $child_id );
+ $child = wc_get_product( $child_id );
+
+ // Ensure we have a valid child product.
+ if ( ! $child ) {
+ wc_get_logger()->warning(
+ sprintf(
+ /* translators: 1: child product ID 2: parent product ID. */
+ __( 'Unable to load child product %1$d while adjusting download permissions for product %2$d.', 'woocommerce' ),
+ $child_id,
+ $product_id
+ )
+ );
+ continue;
+ }
+
$children_with_downloads[ $child_id ] = $this->get_download_files_and_permissions( $child );
}
From 9bfd8535f7c15cb1eb528e028ed5a9571512a9bc Mon Sep 17 00:00:00 2001
From: barryhughes <3594411+barryhughes@users.noreply.github.com>
Date: Tue, 17 Jan 2023 15:54:59 -0800
Subject: [PATCH 22/58] Be more specific: we only expect WC_Product types at
this point.
---
.../woocommerce/src/Internal/DownloadPermissionsAdjuster.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php b/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
index b631f92e489..f2f3adb5949 100644
--- a/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
+++ b/plugins/woocommerce/src/Internal/DownloadPermissionsAdjuster.php
@@ -6,6 +6,7 @@
namespace Automattic\WooCommerce\Internal;
use Automattic\WooCommerce\Proxies\LegacyProxy;
+use WC_Product;
defined( 'ABSPATH' ) || exit;
@@ -102,7 +103,7 @@ class DownloadPermissionsAdjuster {
$child = wc_get_product( $child_id );
// Ensure we have a valid child product.
- if ( ! $child ) {
+ if ( ! $child instanceof WC_Product ) {
wc_get_logger()->warning(
sprintf(
/* translators: 1: child product ID 2: parent product ID. */
From e9a4d6c9ba3fc038f20e14288f21a8938a997735 Mon Sep 17 00:00:00 2001
From: Chi-Hsuan Huang
Date: Wed, 18 Jan 2023 11:56:33 +0800
Subject: [PATCH 23/58] Add an optional "InputProps" to experimental
SelectControl component (#36470)
* Add inputProps to experimental SelectControl component so we can pass custom props to the input
* Add changelog
---
.../js/components/changelog/add-input-props-to-exp-select | 4 ++++
.../src/experimental-select-control/select-control.tsx | 4 ++++
2 files changed, 8 insertions(+)
create mode 100644 packages/js/components/changelog/add-input-props-to-exp-select
diff --git a/packages/js/components/changelog/add-input-props-to-exp-select b/packages/js/components/changelog/add-input-props-to-exp-select
new file mode 100644
index 00000000000..a4123d1f6ea
--- /dev/null
+++ b/packages/js/components/changelog/add-input-props-to-exp-select
@@ -0,0 +1,4 @@
+Significance: patch
+Type: add
+
+Add an optional "InputProps" to experimental SelectControl component
diff --git a/packages/js/components/src/experimental-select-control/select-control.tsx b/packages/js/components/src/experimental-select-control/select-control.tsx
index e811d1d3e4e..9fece1fa36d 100644
--- a/packages/js/components/src/experimental-select-control/select-control.tsx
+++ b/packages/js/components/src/experimental-select-control/select-control.tsx
@@ -7,6 +7,7 @@ import {
UseComboboxState,
UseComboboxStateChangeOptions,
useMultipleSelection,
+ GetInputPropsOptions,
} from 'downshift';
import {
useState,
@@ -65,6 +66,7 @@ export type SelectControlProps< ItemType > = {
selected: ItemType | ItemType[] | null;
className?: string;
disabled?: boolean;
+ inputProps?: GetInputPropsOptions;
suffix?: JSX.Element | null;
/**
* This is a feature already implemented in downshift@7.0.0 through the
@@ -119,6 +121,7 @@ function SelectControl< ItemType = DefaultItemType >( {
selected,
className,
disabled,
+ inputProps = {},
suffix = ,
__experimentalOpenMenuOnFocus = false,
}: SelectControlProps< ItemType > ) {
@@ -268,6 +271,7 @@ function SelectControl< ItemType = DefaultItemType >( {
onBlur: () => setIsFocused( false ),
placeholder,
disabled,
+ ...inputProps,
} ) }
suffix={ suffix }
>
From b1f80c1271a4d09e3b339266fe012078c960b02c Mon Sep 17 00:00:00 2001
From: Sam Seay
Date: Wed, 18 Jan 2023 18:42:46 +1300
Subject: [PATCH 24/58] Create a manually triggered workflow to release
WooCommerce Beta Tester (#36387)
---
.github/workflows/release-wc-beta-tester.yml | 34 ++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 .github/workflows/release-wc-beta-tester.yml
diff --git a/.github/workflows/release-wc-beta-tester.yml b/.github/workflows/release-wc-beta-tester.yml
new file mode 100644
index 00000000000..c2e6c7adf45
--- /dev/null
+++ b/.github/workflows/release-wc-beta-tester.yml
@@ -0,0 +1,34 @@
+name: WooCommerce Beta Tester Release
+permissions: {}
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'The version number for the release'
+ required: true
+
+jobs:
+ release:
+ name: Run release scripts
+ runs-on: ubuntu-20.04
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Setup WooCommerce Monorepo
+ uses: ./.github/actions/setup-woocommerce-monorepo
+
+ - name: Build WooCommerce Beta Tester Zip
+ working-directory: plugins/woocommerce-beta-tester
+ run: pnpm build:zip
+
+ - name: Create release
+ id: create_release
+ uses: woocommerce/action-gh-release@master
+ with:
+ tag_name: wc-beta-tester-${{ inputs.version }}
+ name: WooCommerce Beta Tester Release ${{ inputs.version }}
+ draft: false
+ files: plugins/woocommerce-beta-tester/woocommerce-beta-tester.zip
From 141a0bd6b419d7f96f7f566e1217b0d272c2b968 Mon Sep 17 00:00:00 2001
From: Sam Seay
Date: Wed, 18 Jan 2023 18:43:00 +1300
Subject: [PATCH 25/58] Fix release post template syntax errors and add
prettierignore (#36411)
Fix some template issues, add prettierignore to disable prettier on ejs files.
---
tools/release-posts/.prettierignore | 1 +
tools/release-posts/templates/database.ejs | 1 +
tools/release-posts/templates/oauth.ejs | 2 +-
tools/release-posts/templates/release.ejs | 1 +
tools/release-posts/templates/templates.ejs | 1 +
5 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 tools/release-posts/.prettierignore
diff --git a/tools/release-posts/.prettierignore b/tools/release-posts/.prettierignore
new file mode 100644
index 00000000000..6e551732dd9
--- /dev/null
+++ b/tools/release-posts/.prettierignore
@@ -0,0 +1 @@
+*.ejs
diff --git a/tools/release-posts/templates/database.ejs b/tools/release-posts/templates/database.ejs
index 6f5f9016bf2..de3fea44d6f 100644
--- a/tools/release-posts/templates/database.ejs
+++ b/tools/release-posts/templates/database.ejs
@@ -49,6 +49,7 @@
+
<% } else { %>
There are no database changes in this release.
diff --git a/tools/release-posts/templates/oauth.ejs b/tools/release-posts/templates/oauth.ejs
index 5228670644e..248ea63d171 100644
--- a/tools/release-posts/templates/oauth.ejs
+++ b/tools/release-posts/templates/oauth.ejs
@@ -3,5 +3,5 @@
Authentication Success
- Authentication successful, please return the console to complete the process.
+ Authentication successful, please return to the console to complete the process.