From ebb43378a1198fbd9a380fbb0b0338ff4f58e603 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 6 Apr 2023 20:46:59 +0530 Subject: [PATCH 01/33] Remove unique constraint from order_key to prevent empty key conflict. --- .../changelog/fix-order_key_migration | 4 ++++ .../DataStores/Orders/OrdersTableDataStore.php | 2 +- .../PostsToOrdersMigrationControllerTest.php | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-order_key_migration diff --git a/plugins/woocommerce/changelog/fix-order_key_migration b/plugins/woocommerce/changelog/fix-order_key_migration new file mode 100644 index 00000000000..6d6b7f5a067 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-order_key_migration @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Remove unique constraint from order_key, since orders can be created with emtpy order key, which conflicts with the constraint. diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 4050de796f1..1de11b44f65 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -2412,7 +2412,7 @@ CREATE TABLE $operational_data_table_name ( discount_total_amount decimal(26, 8) NULL, recorded_sales tinyint(1) NULL, UNIQUE KEY order_id (order_id), - UNIQUE KEY order_key (order_key) + KEY order_key (order_key) ) $collate; CREATE TABLE $meta_table ( id bigint(20) unsigned auto_increment primary key, diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php index 1670e5444c3..739d518724f 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php @@ -746,4 +746,21 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in $this->assertEmpty( $errors ); } + + /** + * @testDox Test migration for multiple null order_key meta value. + */ + public function test_order_key_null_multiple() { + $order1 = OrderHelper::create_order(); + $order2 = OrderHelper::create_order(); + delete_post_meta( $order1->get_id(), '_order_key' ); + delete_post_meta( $order2->get_id(), '_order_key' ); + + $this->sut->migrate_order( $order1->get_id() ); + $this->sut->migrate_order( $order2->get_id() ); + + $errors = $this->sut->verify_migrated_orders( array( $order1->get_id(), $order2->get_id() ) ); + + $this->assertEmpty( $errors ); + } } From 322639bb7efb4fe89cc651e9ab201b58e7e20d75 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 6 Apr 2023 21:06:38 +0530 Subject: [PATCH 02/33] Extra protection for empty order key orders. Some order can have order key set to empty string. This commit disallows fetching those orders via key at DB level (its already disallowed from interface). --- .../src/Internal/DataStores/Orders/OrdersTableDataStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 1de11b44f65..f9d15c1e53a 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -847,7 +847,7 @@ WHERE $wpdb->prepare( "SELECT {$orders_table}.id FROM {$orders_table} INNER JOIN {$op_table} ON {$op_table}.order_id = {$orders_table}.id - WHERE {$op_table}.order_key = %s", + WHERE {$op_table}.order_key = %s AND {$op_table}.order_key != ''", $order_key ) ); From d4d375e8742f3a95aa565b3d8c50f7bed16271c0 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:51:38 -0700 Subject: [PATCH 03/33] WC_Data: Add method `delete_matched_meta_data` Brings the CRUD layer's meta data handling closer to parity with WP by allowing for selectively deleting meta entries with a specific key only if they contain a specific value. Fixes #37650 --- .../includes/abstracts/abstract-wc-data.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php index 863db390320..2f19cb6d1f0 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php @@ -506,6 +506,26 @@ abstract class WC_Data { } } + /** + * Delete meta data. + * + * @since 7.7.0 + * @param string $key Meta key. + * @param mixed $value Meta value. Entries will only be removed that match the value. + */ + public function delete_matched_meta_data( $key, $value ) { + $this->maybe_read_meta_data(); + $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true ); + + if ( $array_keys ) { + foreach ( $array_keys as $array_key ) { + if ( $value === $this->meta_data[ $array_key ]->value ) { + $this->meta_data[ $array_key ]->value = null; + } + } + } + } + /** * Delete meta data. * From d8ec0490cb534505d1c4003af76648756cc72e94 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:55:27 -0700 Subject: [PATCH 04/33] Add unit test --- .../tests/legacy/unit-tests/crud/data.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php b/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php index e4a4c4ce33b..dd1c2c6bcc2 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/crud/data.php @@ -325,6 +325,30 @@ class WC_Tests_CRUD_Data extends WC_Unit_Test_Case { $this->assertEmpty( $object->get_meta( 'test_meta_key' ) ); } + /** + * Test deleting meta selectively. + */ + public function test_delete_matched_meta_data() { + $object = $this->create_test_post(); + $object_id = $object->get_id(); + add_metadata( 'post', $object_id, 'test_meta_key', 'val1' ); + add_metadata( 'post', $object_id, 'test_meta_key', 'val2' ); + add_metadata( 'post', $object_id, 'test_meta_key', array( 'foo', 'bar' ) ); + $object = new WC_Mock_WC_Data( $object_id ); + + $this->assertCount( 3, $object->get_meta( 'test_meta_key', false ) ); + + $object->delete_matched_meta_data( 'test_meta_key', 'val1' ); + $this->assertCount( 2, $object->get_meta( 'test_meta_key', false ) ); + + $object->delete_matched_meta_data( 'test_meta_key', array( 'bar', 'baz' ) ); + $this->assertCount( 2, $object->get_meta( 'test_meta_key', false ) ); + + $object->delete_matched_meta_data( 'test_meta_key', array( 'foo', 'bar' ) ); + $this->assertCount( 1, $object->get_meta( 'test_meta_key', false ) ); + + $this->assertEquals( 'val2', $object->get_meta( 'test_meta_key' ) ); + } /** * Test saving metadata (Actually making sure changes are written to DB). From b389a4e8aef1e3e0ee7863470b70be1746abc5b1 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 11 Apr 2023 15:58:27 -0700 Subject: [PATCH 05/33] Add changelog file --- plugins/woocommerce/changelog/fix-37650-delete-meta-data | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-37650-delete-meta-data diff --git a/plugins/woocommerce/changelog/fix-37650-delete-meta-data b/plugins/woocommerce/changelog/fix-37650-delete-meta-data new file mode 100644 index 00000000000..a9c72a79091 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-37650-delete-meta-data @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add method delete_matched_meta_data to WC_Data objects From 3c64b953a0b3d3b32deaa663074092b28026ecc9 Mon Sep 17 00:00:00 2001 From: Corey McKrill <916023+coreymckrill@users.noreply.github.com> Date: Tue, 11 Apr 2023 16:02:01 -0700 Subject: [PATCH 06/33] Update doc block --- plugins/woocommerce/includes/abstracts/abstract-wc-data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php index 2f19cb6d1f0..06ac6cc11ae 100644 --- a/plugins/woocommerce/includes/abstracts/abstract-wc-data.php +++ b/plugins/woocommerce/includes/abstracts/abstract-wc-data.php @@ -507,7 +507,7 @@ abstract class WC_Data { } /** - * Delete meta data. + * Delete meta data with a matching value. * * @since 7.7.0 * @param string $key Meta key. From 55e07451ce99a0a3438a233dfc67695d25ea9bfc Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 12 Apr 2023 15:33:23 +0530 Subject: [PATCH 07/33] Add unit test for asserting that first meta is migrated. --- .../PostsToOrdersMigrationControllerTest.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php index 1670e5444c3..22b2fbbccaa 100644 --- a/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php +++ b/plugins/woocommerce/tests/php/src/Database/Migrations/CustomOrderTable/PostsToOrdersMigrationControllerTest.php @@ -746,4 +746,33 @@ WHERE order_id = {$order_id} AND meta_key = 'non_unique_key_1' AND meta_value in $this->assertEmpty( $errors ); } + + /** + * @testDox When there are mutli meta values for a supposed unique meta key, the first one is picked. + */ + public function test_first_value_is_picked_when_multi_value() { + global $wpdb; + $order = wc_get_order( OrderHelper::create_complex_wp_post_order() ); + $original_order_key = $order->get_order_key(); + + $this->assertNotEmpty( $original_order_key ); + + // Add a second order key. + add_post_meta( $order->get_id(), '_order_key', 'second_order_key_should_be_ignored' ); + + $this->sut->migrate_order( $order->get_id() ); + + $migrated_order_key = $wpdb->get_var( + $wpdb->prepare( + "SELECT order_key FROM {$wpdb->prefix}wc_order_operational_data WHERE order_id = %d", + $order->get_id() + ) + ); + + $this->assertEquals( $original_order_key, $migrated_order_key ); + + $errors = $this->sut->verify_migrated_orders( array( $order->get_id() ) ); + + $this->assertEmpty( $errors ); + } } From d5211bbaa65fda427503cefc11c90e94dd872177 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 12 Apr 2023 15:38:58 +0530 Subject: [PATCH 08/33] Use first meta value instead of last to be consistent with WP_Post. --- .../src/Database/Migrations/MetaToCustomTableMigrator.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index 71b0d3022a4..c72ab967a3a 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -543,7 +543,11 @@ WHERE private function processs_and_sanitize_meta_data( array &$sanitized_entity_data, array &$error_records, array $meta_data ): void { foreach ( $meta_data as $datum ) { $column_schema = $this->meta_column_mapping[ $datum->meta_key ]; - $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); + if ( isset( $sanitized_entity_data[ $datum->entity_id ][ $column_schema['destination'] ] ) ) { + // We pick only the first meta if there are duplicates for a flat column, to be consistent with WP core behavior in handing duplicate meta which are marked as unique. + continue; + } + $value = $this->validate_data( $datum->meta_value, $column_schema['type'] ); if ( is_wp_error( $value ) ) { $error_records[ $datum->entity_id ][ $column_schema['destination'] ] = "{$value->get_error_code()}: {$value->get_error_message()}"; } else { From 2147d2abcf2a27ac30f8770a2fb7e444c711fb93 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 12 Apr 2023 15:39:44 +0530 Subject: [PATCH 09/33] Add changelog. --- plugins/woocommerce/changelog/fix-37660 | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 plugins/woocommerce/changelog/fix-37660 diff --git a/plugins/woocommerce/changelog/fix-37660 b/plugins/woocommerce/changelog/fix-37660 new file mode 100644 index 00000000000..19479c9b9bf --- /dev/null +++ b/plugins/woocommerce/changelog/fix-37660 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Use first meta value for HPOS migration when there are duplicates for flat column. From 4a5db60c2af550576f99e1b284c6e5438bae4d6c Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 12 Apr 2023 18:08:22 +0530 Subject: [PATCH 10/33] Adjust verification so that it only checks the first meta value. --- .../PostMetaToOrderMetaMigrator.php | 1 + .../PostToOrderAddressTableMigrator.php | 1 + .../PostToOrderOpTableMigrator.php | 1 + .../PostToOrderTableMigrator.php | 1 + .../Migrations/MetaToCustomTableMigrator.php | 80 +++++++++++++++---- 5 files changed, 67 insertions(+), 17 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php index a10720d2b4f..85a8ea5555f 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostMetaToOrderMetaMigrator.php @@ -52,6 +52,7 @@ class PostMetaToOrderMetaMigrator extends MetaToMetaTableMigrator { 'meta' => array( 'table_name' => $wpdb->postmeta, 'entity_id_column' => 'post_id', + 'meta_id_column' => 'meta_id', 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', ), diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php index a9aba4bf6dd..526bd0a7da0 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderAddressTableMigrator.php @@ -56,6 +56,7 @@ class PostToOrderAddressTableMigrator extends MetaToCustomTableMigrator { ), 'meta' => array( 'table_name' => $wpdb->postmeta, + 'meta_id_column' => 'meta_id', 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', 'entity_id_column' => 'post_id', diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php index 05f22f6d1d0..995540baaf8 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderOpTableMigrator.php @@ -40,6 +40,7 @@ class PostToOrderOpTableMigrator extends MetaToCustomTableMigrator { ), 'meta' => array( 'table_name' => $wpdb->postmeta, + 'meta_id_column' => 'meta_id', 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', 'entity_id_column' => 'post_id', diff --git a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php index e21085c62ba..8635eb36f7b 100644 --- a/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/CustomOrderTable/PostToOrderTableMigrator.php @@ -39,6 +39,7 @@ class PostToOrderTableMigrator extends MetaToCustomTableMigrator { ), 'meta' => array( 'table_name' => $wpdb->postmeta, + 'meta_id_column' => 'meta_id', 'meta_key_column' => 'meta_key', 'meta_value_column' => 'meta_value', 'entity_id_column' => 'post_id', diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index c72ab967a3a..35829eef75e 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -614,7 +614,7 @@ WHERE $query = $this->build_verification_query( $source_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query should already be prepared. $results = $wpdb->get_results( $query, ARRAY_A ); - + $results = $this->fill_source_metadata( $results, $source_ids ); return $this->verify_data( $results ); } @@ -627,19 +627,13 @@ WHERE */ protected function build_verification_query( $source_ids ) { $source_table = $this->schema_config['source']['entity']['table_name']; - $meta_table = $this->schema_config['source']['meta']['table_name']; $destination_table = $this->schema_config['destination']['table_name']; - $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; - $meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; - $meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; $destination_source_rel_column = $this->schema_config['destination']['source_rel_column']; $source_destination_rel_column = $this->schema_config['source']['entity']['destination_rel_column']; - $source_meta_rel_column = $this->schema_config['source']['entity']['meta_rel_column']; $source_destination_join_clause = "$destination_table ON $destination_table.$destination_source_rel_column = $source_table.$source_destination_rel_column"; $meta_select_clauses = array(); - $meta_join_clauses = array(); $source_select_clauses = array(); $destination_select_clauses = array(); @@ -650,19 +644,11 @@ WHERE } foreach ( $this->meta_column_mapping as $meta_key => $schema ) { - $meta_table_alias = "meta_source_{$schema['destination']}"; - $meta_select_clauses[] = "$meta_table_alias.$meta_value_column AS $meta_table_alias"; - $meta_join_clauses[] = " -$meta_table $meta_table_alias ON - $meta_table_alias.$meta_entity_id_column = $source_table.$source_meta_rel_column AND - $meta_table_alias.$meta_key_column = '$meta_key' -"; $destination_select_clauses[] = "$destination_table.{$schema['destination']} as {$destination_table}_{$schema['destination']}"; } $select_clause = implode( ', ', array_merge( $source_select_clauses, $meta_select_clauses, $destination_select_clauses ) ); - $meta_join_clause = implode( ' LEFT JOIN ', $meta_join_clauses ); $where_clause = $this->get_where_clause_for_verification( $source_ids ); @@ -670,11 +656,71 @@ $meta_table $meta_table_alias ON SELECT $select_clause FROM $source_table LEFT JOIN $source_destination_join_clause - LEFT JOIN $meta_join_clause WHERE $where_clause "; } + /** + * Fill source metadata for given IDS for verification. + * + * @param array $source_ids List of source IDs. + * + * @return array List of source metadata. This will be in the following format: + * [ + * { + * $source_table_$source_column: $value, + * ..., + * $destination_table_$destination_column: $value, + * ... + * meta_source_{$destination_column_name1}: $meta_value, + * ... + * }, + * ... + * ] + */ + private function fill_source_metadata( $results, $source_ids ) { + global $wpdb; + $meta_table = $this->schema_config['source']['meta']['table_name']; + $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; + $meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; + $meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; + $meta_id_column = $this->schema_config['source']['meta']['meta_id_column']; + $meta_columns = array_keys( $this->meta_column_mapping ); + + $meta_columns_placeholder = implode( ', ', array_fill( 0, count( $meta_columns ), '%s' ) ); + $source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) ); + + $query = $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + "SELECT $meta_entity_id_column as entity_id, $meta_key_column as meta_key, $meta_value_column as meta_value + FROM $meta_table + WHERE $meta_entity_id_column IN ($source_ids_placeholder) + AND $meta_key_column IN ($meta_columns_placeholder) + ORDER BY $meta_id_column ASC", + array_merge( $source_ids, $meta_columns ) + ); + + $meta_data = $wpdb->get_results( $query, ARRAY_A ); + $source_metadata_rows = array(); + foreach ( $meta_data as $meta_datum ) { + if ( ! isset( $source_metadata_rows[ $meta_datum['entity_id'] ] ) ) { + $source_metadata_rows[ $meta_datum['entity_id'] ] = array(); + } + $destination_column = $this->meta_column_mapping[ $meta_datum['meta_key'] ]['destination']; + $alias = "meta_source_{$destination_column}"; + if ( isset( $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] ) ) { + // Only process first value, duplicate values mapping to flat columns are ignored to be consistent with WP core. + continue; + } + $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] = $meta_datum['meta_value']; + } + foreach ( $results as $index => $result_row ) { + $source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ]; + $results[ $index ] = array_merge( $result_row, $source_metadata_rows[ $source_id ] ); + } + return $results; + } + /** * Helper function to generate where clause for fetching data for verification. * @@ -802,7 +848,7 @@ WHERE $where_clause $row[ $destination_alias ] = null; } } - if ( is_null( $row[ $alias ] ) ) { + if ( ! isset( $row[ $alias ] ) ) { $row[ $alias ] = $this->get_type_defaults( $schema['type'] ); } From e8363828f7a6a74b166caa55f94ef63e38116dcd Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 12 Apr 2023 18:40:19 +0530 Subject: [PATCH 11/33] Fixup to handle null data. --- .../src/Database/Migrations/MetaToCustomTableMigrator.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index 35829eef75e..ee38fc669c5 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -827,6 +827,9 @@ WHERE $where_clause * @return array Processed row. */ private function pre_process_row( $row, $schema, $alias, $destination_alias ) { + if ( ! isset( $row[ $alias ] ) ) { + $row[ $alias ] = $this->get_type_defaults( $schema['type'] ); + } if ( in_array( $schema['type'], array( 'int', 'decimal' ), true ) ) { if ( '' === $row[ $alias ] || null === $row[ $alias ] ) { $row[ $alias ] = 0; // $wpdb->prepare forces empty values to 0. @@ -848,10 +851,6 @@ WHERE $where_clause $row[ $destination_alias ] = null; } } - if ( ! isset( $row[ $alias ] ) ) { - $row[ $alias ] = $this->get_type_defaults( $schema['type'] ); - } - return $row; } From 408cf92ab0750c37842c6ed5fff2cfc1867348d7 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 13 Apr 2023 12:59:53 +0530 Subject: [PATCH 12/33] Better changelog messaging. Co-authored-by: Barry Hughes <3594411+barryhughes@users.noreply.github.com> --- plugins/woocommerce/changelog/fix-order_key_migration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/changelog/fix-order_key_migration b/plugins/woocommerce/changelog/fix-order_key_migration index 6d6b7f5a067..0dd82f9f565 100644 --- a/plugins/woocommerce/changelog/fix-order_key_migration +++ b/plugins/woocommerce/changelog/fix-order_key_migration @@ -1,4 +1,4 @@ Significance: patch Type: fix -Remove unique constraint from order_key, since orders can be created with emtpy order key, which conflicts with the constraint. +Remove unique constraint from order_key, since orders can be created with empty order keys, which then conflict with the constraint. From 6c22ffe88daaa6bfb2a8566dc77f71775aba1869 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Thu, 13 Apr 2023 13:28:48 +0530 Subject: [PATCH 13/33] Coding standards fixes. --- .../Migrations/MetaToCustomTableMigrator.php | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index ee38fc669c5..7873a9abf84 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -649,7 +649,6 @@ WHERE $select_clause = implode( ', ', array_merge( $source_select_clauses, $meta_select_clauses, $destination_select_clauses ) ); - $where_clause = $this->get_where_clause_for_verification( $source_ids ); return " @@ -661,11 +660,7 @@ WHERE $where_clause } /** - * Fill source metadata for given IDS for verification. - * - * @param array $source_ids List of source IDs. - * - * @return array List of source metadata. This will be in the following format: + * Fill source metadata for given IDS for verification. This will return filled data in following format: * [ * { * $source_table_$source_column: $value, @@ -677,21 +672,26 @@ WHERE $where_clause * }, * ... * ] + * + * @param array $results Entity data from both source and destination table. + * @param array $source_ids List of source IDs. + * + * @return array Filled $results param with source metadata. */ private function fill_source_metadata( $results, $source_ids ) { global $wpdb; - $meta_table = $this->schema_config['source']['meta']['table_name']; - $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; - $meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; - $meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; - $meta_id_column = $this->schema_config['source']['meta']['meta_id_column']; - $meta_columns = array_keys( $this->meta_column_mapping ); + $meta_table = $this->schema_config['source']['meta']['table_name']; + $meta_entity_id_column = $this->schema_config['source']['meta']['entity_id_column']; + $meta_key_column = $this->schema_config['source']['meta']['meta_key_column']; + $meta_value_column = $this->schema_config['source']['meta']['meta_value_column']; + $meta_id_column = $this->schema_config['source']['meta']['meta_id_column']; + $meta_columns = array_keys( $this->meta_column_mapping ); $meta_columns_placeholder = implode( ', ', array_fill( 0, count( $meta_columns ), '%s' ) ); $source_ids_placeholder = implode( ', ', array_fill( 0, count( $source_ids ), '%d' ) ); $query = $wpdb->prepare( - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare "SELECT $meta_entity_id_column as entity_id, $meta_key_column as meta_key, $meta_value_column as meta_value FROM $meta_table WHERE $meta_entity_id_column IN ($source_ids_placeholder) @@ -699,15 +699,17 @@ WHERE $where_clause ORDER BY $meta_id_column ASC", array_merge( $source_ids, $meta_columns ) ); + //phpcs:enable - $meta_data = $wpdb->get_results( $query, ARRAY_A ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $meta_data = $wpdb->get_results( $query, ARRAY_A ); $source_metadata_rows = array(); foreach ( $meta_data as $meta_datum ) { if ( ! isset( $source_metadata_rows[ $meta_datum['entity_id'] ] ) ) { $source_metadata_rows[ $meta_datum['entity_id'] ] = array(); } $destination_column = $this->meta_column_mapping[ $meta_datum['meta_key'] ]['destination']; - $alias = "meta_source_{$destination_column}"; + $alias = "meta_source_{$destination_column}"; if ( isset( $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] ) ) { // Only process first value, duplicate values mapping to flat columns are ignored to be consistent with WP core. continue; @@ -715,7 +717,7 @@ WHERE $where_clause $source_metadata_rows[ $meta_datum['entity_id'] ][ $alias ] = $meta_datum['meta_value']; } foreach ( $results as $index => $result_row ) { - $source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ]; + $source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ]; $results[ $index ] = array_merge( $result_row, $source_metadata_rows[ $source_id ] ); } return $results; From a02d0a8117082754389eb5f6a547b2c2f886b6d5 Mon Sep 17 00:00:00 2001 From: Ilyas Foo Date: Thu, 13 Apr 2023 23:16:45 +0800 Subject: [PATCH 14/33] Fix invalid return callback ref warning (#37655) * Fix invalid return in callback ref and add tests * Changelog --- .../fix-37654-invalid-return-callback-ref | 4 +++ .../src/inbox-note/test/inbox-note.tsx | 2 ++ .../test/use-callback-on-link-click.tsx | 13 +++++++++ .../inbox-note/use-callback-on-link-click.ts | 28 +++++++++++-------- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref diff --git a/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref new file mode 100644 index 00000000000..d14e18f1448 --- /dev/null +++ b/packages/js/experimental/changelog/fix-37654-invalid-return-callback-ref @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Fix invalid return callback ref warning diff --git a/packages/js/experimental/src/inbox-note/test/inbox-note.tsx b/packages/js/experimental/src/inbox-note/test/inbox-note.tsx index f1078577e04..bbe03171804 100644 --- a/packages/js/experimental/src/inbox-note/test/inbox-note.tsx +++ b/packages/js/experimental/src/inbox-note/test/inbox-note.tsx @@ -23,6 +23,8 @@ jest.mock( 'react-visibility-sensor', () => } ) ); +window.open = jest.fn(); + describe( 'InboxNoteCard', () => { const note = { id: 1, diff --git a/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx b/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx index a7c4d334565..2efcb221b98 100644 --- a/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx +++ b/packages/js/experimental/src/inbox-note/test/use-callback-on-link-click.tsx @@ -42,4 +42,17 @@ describe( 'useCallbackOnLinkClick hook', () => { userEvent.click( getByText( 'Button' ) ); expect( callback ).not.toHaveBeenCalled(); } ); + + it( 'should remove listener on unmount', () => { + const listener = jest.fn(); + const { getByText, unmount } = render( + + ); + const span = getByText( 'Some Text' ); + jest.spyOn( span, 'removeEventListener' ).mockImplementation( + listener + ); + unmount(); + expect( listener ).toHaveBeenCalledTimes( 1 ); + } ); } ); diff --git a/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts b/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts index e7243aff958..537c08e0467 100644 --- a/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts +++ b/packages/js/experimental/src/inbox-note/use-callback-on-link-click.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { useCallback } from '@wordpress/element'; +import { useCallback, useEffect, useRef } from '@wordpress/element'; export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) { const onNodeClick = useCallback( @@ -20,17 +20,21 @@ export function useCallbackOnLinkClick( onClick: ( link: string ) => void ) { [ onClick ] ); - return useCallback( - ( node: HTMLElement ) => { + const nodeRef = useRef< HTMLElement | null >( null ); + + useEffect( () => { + const node = nodeRef.current; + if ( node ) { + node.addEventListener( 'click', onNodeClick ); + } + return () => { if ( node ) { - node.addEventListener( 'click', onNodeClick ); + node.removeEventListener( 'click', onNodeClick ); } - return () => { - if ( node ) { - node.removeEventListener( 'click', onNodeClick ); - } - }; - }, - [ onNodeClick ] - ); + }; + }, [ onNodeClick ] ); + + return useCallback( ( node: HTMLElement ) => { + nodeRef.current = node; + }, [] ); } From fb12ad20fd59af2ec522adf3732f7dba9841b01f Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Thu, 13 Apr 2023 08:45:50 -0700 Subject: [PATCH 15/33] Replacing rest_namespace modification with middleware due to blocks issues (#37621) --- .../changelog/fix-rest-namespace-blocks-37619 | 4 +++ packages/js/product-editor/package.json | 1 + packages/js/product-editor/src/utils/index.ts | 1 + .../src/utils/product-apifetch-middleware.ts | 34 +++++++++++++++++++ .../hooks/use-product-entity-record.ts | 1 - .../client/products/product-page.tsx | 3 ++ .../changelog/fix-rest-namespace-blocks-37619 | 4 +++ .../includes/class-wc-post-types.php | 1 - .../Features/ProductBlockEditor/Init.php | 12 ------- pnpm-lock.yaml | 15 ++++---- 10 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 create mode 100644 packages/js/product-editor/src/utils/product-apifetch-middleware.ts create mode 100644 plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619 diff --git a/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 b/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 new file mode 100644 index 00000000000..a6315f79155 --- /dev/null +++ b/packages/js/product-editor/changelog/fix-rest-namespace-blocks-37619 @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Adding apifetch middleware to override product api endpoint only for the product editor. diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json index f508c8d9d77..49b271d990a 100644 --- a/packages/js/product-editor/package.json +++ b/packages/js/product-editor/package.json @@ -41,6 +41,7 @@ "@woocommerce/number": "workspace:*", "@woocommerce/settings": "^1.0.0", "@woocommerce/tracks": "workspace:^1.3.0", + "@wordpress/api-fetch": "wp-6.0", "@wordpress/block-editor": "^9.8.0", "@wordpress/blocks": "^12.3.0", "@wordpress/components": "wp-6.0", diff --git a/packages/js/product-editor/src/utils/index.ts b/packages/js/product-editor/src/utils/index.ts index 67a0014effc..31d0ce7f148 100644 --- a/packages/js/product-editor/src/utils/index.ts +++ b/packages/js/product-editor/src/utils/index.ts @@ -22,6 +22,7 @@ import { preventLeavingProductForm } from './prevent-leaving-product-form'; export * from './create-ordered-children'; export * from './sort-fills-by-order'; export * from './init-blocks'; +export * from './product-apifetch-middleware'; export { AUTO_DRAFT_NAME, diff --git a/packages/js/product-editor/src/utils/product-apifetch-middleware.ts b/packages/js/product-editor/src/utils/product-apifetch-middleware.ts new file mode 100644 index 00000000000..b33c5d8ca71 --- /dev/null +++ b/packages/js/product-editor/src/utils/product-apifetch-middleware.ts @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { getQuery } from '@woocommerce/navigation'; + +const isProductEditor = () => { + const query: { page?: string; path?: string } = getQuery(); + return ( + query?.page === 'wc-admin' && + [ '/add-product', '/product/' ].some( ( path ) => + query?.path?.startsWith( path ) + ) + ); +}; + +export const productApiFetchMiddleware = () => { + // This is needed to ensure that we use the correct namespace for the entity data store + // without disturbing the rest_namespace outside of the product block editor. + apiFetch.use( ( options, next ) => { + const versionTwoRegex = new RegExp( '^/wp/v2/product' ); + if ( + options.path && + versionTwoRegex.test( options?.path ) && + isProductEditor() + ) { + options.path = options.path.replace( + versionTwoRegex, + '/wc/v3/products' + ); + } + return next( options ); + } ); +}; diff --git a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts index b0f03086810..823e371869d 100644 --- a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts +++ b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts @@ -4,7 +4,6 @@ import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor'; import { Product } from '@woocommerce/data'; import { useDispatch, resolveSelect } from '@wordpress/data'; - import { useEffect, useState } from '@wordpress/element'; export function useProductEntityRecord( diff --git a/plugins/woocommerce-admin/client/products/product-page.tsx b/plugins/woocommerce-admin/client/products/product-page.tsx index 6b414978f5a..2f67ab5fadb 100644 --- a/plugins/woocommerce-admin/client/products/product-page.tsx +++ b/plugins/woocommerce-admin/client/products/product-page.tsx @@ -4,6 +4,7 @@ import { __experimentalEditor as Editor, ProductEditorSettings, + productApiFetchMiddleware, } from '@woocommerce/product-editor'; import { Spinner } from '@wordpress/components'; @@ -20,6 +21,8 @@ import './fills/product-block-editor-fills'; declare const productBlockEditorSettings: ProductEditorSettings; +productApiFetchMiddleware(); + export default function ProductPage() { const { productId } = useParams(); diff --git a/plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619 b/plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619 new file mode 100644 index 00000000000..683a9b7bc9f --- /dev/null +++ b/plugins/woocommerce/changelog/fix-rest-namespace-blocks-37619 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Removing modification to rest_namespace on post type and replacing with middleware. diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php index e2c74e2afe7..a115f340190 100644 --- a/plugins/woocommerce/includes/class-wc-post-types.php +++ b/plugins/woocommerce/includes/class-wc-post-types.php @@ -365,7 +365,6 @@ class WC_Post_Types { 'has_archive' => $has_archive, 'show_in_nav_menus' => true, 'show_in_rest' => true, - 'rest_namespace' => 'wp/v3', 'template' => array( array( 'woocommerce/product-tab', diff --git a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php index b907226e10f..3f81ba822d7 100644 --- a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php +++ b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php @@ -33,7 +33,6 @@ class Init { } if ( Features::is_enabled( self::FEATURE_ID ) ) { add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); - add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_rest_base_config' ) ); $block_registry = new BlockRegistry(); $block_registry->register_product_blocks(); } @@ -106,15 +105,4 @@ class Init { return $link; } - /** - * Updates the product endpoint to use WooCommerce REST API. - * - * @param array $post_args Args for the product post type. - * @return array - */ - public function add_rest_base_config( $post_args ) { - $post_args['rest_base'] = 'products'; - $post_args['rest_namespace'] = 'wc/v3'; - return $post_args; - } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94584ec418c..9d24fa04cd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1441,6 +1441,7 @@ importers: '@woocommerce/number': workspace:* '@woocommerce/settings': ^1.0.0 '@woocommerce/tracks': workspace:^1.3.0 + '@wordpress/api-fetch': wp-6.0 '@wordpress/block-editor': ^9.8.0 '@wordpress/blocks': ^12.3.0 '@wordpress/browserslist-config': wp-6.0 @@ -1496,6 +1497,7 @@ importers: '@woocommerce/number': link:../number '@woocommerce/settings': 1.0.0 '@woocommerce/tracks': link:../tracks + '@wordpress/api-fetch': 6.3.1 '@wordpress/block-editor': 9.8.0_mtk4wljkd5jimhszw4p7nnxuzm '@wordpress/blocks': 12.5.0_react@17.0.2 '@wordpress/components': 19.8.5_eqi5qhcxfphl6j3pngzexvnehi @@ -9078,7 +9080,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.17.8 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.17.8 '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.17.8 '@babel/types': 7.21.3 @@ -9091,7 +9093,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.3 - '@babel/helper-plugin-utils': 7.19.0 + '@babel/helper-plugin-utils': 7.20.2 '@babel/plugin-proposal-unicode-property-regex': 7.18.6_@babel+core@7.21.3 '@babel/plugin-transform-dotall-regex': 7.18.6_@babel+core@7.21.3 '@babel/types': 7.21.3 @@ -17215,7 +17217,7 @@ packages: '@wordpress/style-engine': 0.15.0 '@wordpress/token-list': 2.19.0 '@wordpress/url': 3.29.0 - '@wordpress/warning': 2.28.0 + '@wordpress/warning': 2.19.0 '@wordpress/wordcount': 3.19.0 change-case: 4.1.2 classnames: 2.3.1 @@ -20624,8 +20626,8 @@ packages: peerDependencies: postcss: ^8.1.0 dependencies: - browserslist: 4.21.4 - caniuse-lite: 1.0.30001418 + browserslist: 4.20.2 + caniuse-lite: 1.0.30001352 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 @@ -22182,7 +22184,6 @@ packages: escalade: 3.1.1 node-releases: 2.0.6 picocolors: 1.0.0 - dev: true /browserslist/4.20.4: resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==} @@ -38056,7 +38057,7 @@ packages: is-touch-device: 1.0.1 lodash: 4.17.21 moment: 2.29.4 - object.assign: 4.1.2 + object.assign: 4.1.4 object.values: 1.1.5 prop-types: 15.8.1 raf: 3.4.1 From 038b97a3188af44dbefe40ca8755bea026bda6a2 Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Thu, 13 Apr 2023 08:56:47 -0700 Subject: [PATCH 16/33] Add edit product link modal under name field (#37612) --- .../changelog/add-product-link-edit-37610 | 4 + .../components/details-name-block/edit.tsx | 175 +++++++++++++++--- .../components/details-name-block/style.scss | 25 +++ .../details-name-field/details-name-field.tsx | 31 +++- .../edit-product-link-modal.tsx | 60 ++---- .../test/edit-product-link-modal.test.tsx | 18 ++ packages/js/product-editor/src/style.scss | 1 + 7 files changed, 251 insertions(+), 63 deletions(-) create mode 100644 packages/js/product-editor/changelog/add-product-link-edit-37610 create mode 100644 packages/js/product-editor/src/components/details-name-block/style.scss diff --git a/packages/js/product-editor/changelog/add-product-link-edit-37610 b/packages/js/product-editor/changelog/add-product-link-edit-37610 new file mode 100644 index 00000000000..ab36038aa75 --- /dev/null +++ b/packages/js/product-editor/changelog/add-product-link-edit-37610 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Refactoring product link modal and adding link to product block editor. diff --git a/packages/js/product-editor/src/components/details-name-block/edit.tsx b/packages/js/product-editor/src/components/details-name-block/edit.tsx index ea38e9a0703..ee06d15d53b 100644 --- a/packages/js/product-editor/src/components/details-name-block/edit.tsx +++ b/packages/js/product-editor/src/components/details-name-block/edit.tsx @@ -2,36 +2,169 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { createElement, createInterpolateElement } from '@wordpress/element'; -import { TextControl } from '@woocommerce/components'; +import { + createElement, + Fragment, + createInterpolateElement, + useState, +} from '@wordpress/element'; + import { useBlockProps } from '@wordpress/block-editor'; +import { cleanForSlug } from '@wordpress/url'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + PRODUCTS_STORE_NAME, + WCDataSelector, + Product, +} from '@woocommerce/data'; +import { + Button, + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore No types for this exist yet. // eslint-disable-next-line @woocommerce/dependency-group -import { useEntityProp } from '@wordpress/core-data'; +import { useEntityProp, useEntityId } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { AUTO_DRAFT_NAME } from '../../utils'; +import { EditProductLinkModal } from '../edit-product-link-modal'; +import { useValidation } from '../../hooks/use-validation'; export function Edit() { const blockProps = useBlockProps(); - const [ name, setName ] = useEntityProp( 'postType', 'product', 'name' ); + + const { editEntityRecord, saveEntityRecord } = useDispatch( 'core' ); + + const [ showProductLinkEditModal, setShowProductLinkEditModal ] = + useState( false ); + + const productId = useEntityId( 'postType', 'product' ); + const product: Product = useSelect( ( select ) => + select( 'core' ).getEditedEntityRecord( + 'postType', + 'product', + productId + ) + ); + + const [ name, setName ] = useEntityProp< string >( + 'postType', + 'product', + 'name' + ); + + const { permalinkPrefix, permalinkSuffix } = useSelect( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ( select: WCDataSelector ) => { + const { getPermalinkParts } = select( PRODUCTS_STORE_NAME ); + if ( productId ) { + const parts = getPermalinkParts( productId ); + return { + permalinkPrefix: parts?.prefix, + permalinkSuffix: parts?.suffix, + }; + } + return {}; + } + ); + + const nameIsValid = useValidation( + 'product/name', + () => Boolean( name ) && name !== AUTO_DRAFT_NAME + ); return ( -
- ', 'woocommerce' ), - { - required: ( - - { __( '(required)', 'woocommerce' ) } - - ), - } + <> +
+ ', 'woocommerce' ), + { + required: ( + + { __( '*', 'woocommerce' ) } + + ), + } + ) } + > + + + { productId && + nameIsValid && + [ 'publish', 'draft' ].includes( product.status ) && + permalinkPrefix && ( + + { __( 'Product link', 'woocommerce' ) } + :  + + { permalinkPrefix } + { product.slug || cleanForSlug( name ) } + { permalinkSuffix } + + + + ) } + { showProductLinkEditModal && ( + setShowProductLinkEditModal( false ) } + onSaved={ () => setShowProductLinkEditModal( false ) } + saveHandler={ async ( updatedSlug ) => { + const { slug, permalink }: Product = + await saveEntityRecord( 'postType', 'product', { + id: product.id, + slug: updatedSlug, + } ); + + if ( slug && permalink ) { + editEntityRecord( + 'postType', + 'product', + product.id, + { + slug, + permalink, + } + ); + + return { + slug, + permalink, + }; + } + } } + /> ) } - name={ 'woocommerce-product-name' } - placeholder={ __( 'e.g. 12 oz Coffee Mug', 'woocommerce' ) } - onChange={ setName } - value={ name || '' } - /> -
+
+ ); } diff --git a/packages/js/product-editor/src/components/details-name-block/style.scss b/packages/js/product-editor/src/components/details-name-block/style.scss new file mode 100644 index 00000000000..b6e6e66c221 --- /dev/null +++ b/packages/js/product-editor/src/components/details-name-block/style.scss @@ -0,0 +1,25 @@ +.product-details-section { + + &__product-link { + color: #757575; + font-size: 12px; + display: block; + margin-top: $gap-smaller; + + > a { + color: inherit; + text-decoration: none; + font-weight: 600; + } + + .components-button.is-link { + font-size: 12px; + text-decoration: none; + margin-left: $gap-smaller; + } + } +} + +.woocommerce-product-form__required-input { + color: #CC1818; +} diff --git a/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx b/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx index 498c1f016e6..b964a445da3 100644 --- a/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx +++ b/packages/js/product-editor/src/components/details-name-field/details-name-field.tsx @@ -22,11 +22,13 @@ import { */ import { PRODUCT_DETAILS_SLUG } from '../../constants'; import { EditProductLinkModal } from '../edit-product-link-modal'; +import { useProductHelper } from '../../hooks/use-product-helper'; export const DetailsNameField = ( {} ) => { + const { updateProductWithStatus } = useProductHelper(); const [ showProductLinkEditModal, setShowProductLinkEditModal ] = useState( false ); - const { getInputProps, values, touched, errors, setValue } = + const { getInputProps, values, touched, errors, setValue, resetForm } = useFormContext< Product >(); const { permalinkPrefix, permalinkSuffix } = useSelect( @@ -102,6 +104,33 @@ export const DetailsNameField = ( {} ) => { product={ values } onCancel={ () => setShowProductLinkEditModal( false ) } onSaved={ () => setShowProductLinkEditModal( false ) } + saveHandler={ async ( slug ) => { + const updatedProduct = await updateProductWithStatus( + values.id, + { + slug, + }, + values.status, + true + ); + if ( updatedProduct && updatedProduct.id ) { + // only reset the updated slug and permalink fields. + resetForm( + { + ...values, + slug: updatedProduct.slug, + permalink: updatedProduct.permalink, + }, + touched, + errors + ); + + return { + slug: updatedProduct.slug, + permalink: updatedProduct.permalink, + }; + } + } } /> ) } diff --git a/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx b/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx index fab86bce6f5..9f1170e0937 100644 --- a/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx +++ b/packages/js/product-editor/src/components/edit-product-link-modal/edit-product-link-modal.tsx @@ -11,20 +11,17 @@ import { import { useDispatch } from '@wordpress/data'; import { cleanForSlug } from '@wordpress/url'; import { Product } from '@woocommerce/data'; -import { useFormContext } from '@woocommerce/components'; import { recordEvent } from '@woocommerce/tracks'; -/** - * Internal dependencies - */ -import { useProductHelper } from '../../hooks/use-product-helper'; - type EditProductLinkModalProps = { product: Product; permalinkPrefix: string; permalinkSuffix: string; onCancel: () => void; onSaved: () => void; + saveHandler: ( + slug: string + ) => Promise< { slug: string; permalink: string } | undefined >; }; export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { @@ -33,14 +30,13 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { permalinkSuffix, onCancel, onSaved, + saveHandler, } ) => { const { createNotice } = useDispatch( 'core/notices' ); - const { updateProductWithStatus, isUpdatingDraft, isUpdatingPublished } = - useProductHelper(); + const [ isSaving, setIsSaving ] = useState< boolean >( false ); const [ slug, setSlug ] = useState( product.slug || cleanForSlug( product.name ) ); - const { resetForm, touched, errors } = useFormContext< Product >(); const onSave = async () => { recordEvent( 'product_update_slug', { @@ -48,35 +44,19 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { product_id: product.id, product_type: product.type, } ); - const updatedProduct = await updateProductWithStatus( - product.id, - { - slug, - }, - product.status, - true - ); - if ( updatedProduct && updatedProduct.id ) { - // only reset the updated slug and permalink fields. - resetForm( - { - ...product, - slug: updatedProduct.slug, - permalink: updatedProduct.permalink, - }, - touched, - errors - ); + + const { slug: updatedSlug, permalink: updatedPermalink } = + ( await saveHandler( slug ) ) ?? {}; + + if ( updatedSlug ) { createNotice( - updatedProduct.slug === cleanForSlug( slug ) - ? 'success' - : 'info', - updatedProduct.slug === cleanForSlug( slug ) + updatedSlug === cleanForSlug( slug ) ? 'success' : 'info', + updatedSlug === cleanForSlug( slug ) ? __( 'Product link successfully updated.', 'woocommerce' ) : __( 'Product link already existed, updated to ', 'woocommerce' - ) + updatedProduct.permalink + ) + updatedPermalink ); } else { createNotice( @@ -122,14 +102,12 @@ export const EditProductLinkModal: React.FC< EditProductLinkModalProps > = ( { + { isOpen && ( + setOpen( false ) } + > + + ! value || + listItems.findIndex( + ( item ) => item.label === typedValue + ) === -1 + } + createValue={ value } + // eslint-disable-next-line no-alert + onCreateNew={ () => alert( 'create new called' ) } + onInputChange={ ( a ) => setValue( a || '' ) } + onSelect={ ( selectedItems ) => { + if ( Array.isArray( selectedItems ) ) { + setSelected( [ + ...selected, + ...selectedItems, + ] ); + } + } } + onRemove={ ( removedItems ) => { + const newValues = Array.isArray( removedItems ) + ? selected.filter( + ( item ) => + ! removedItems.some( + ( { value: removedValue } ) => + item.value === removedValue + ) + ) + : selected.filter( + ( item ) => + item.value !== removedItems.value + ); + setSelected( newValues ); + } } + /> + + ) } + + + ); +}; + export default { title: 'WooCommerce Admin/experimental/SelectTreeControl', component: SelectTree, diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 99d39bf3fa9..e971ee0d231 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -99,7 +99,10 @@ export { TreeControl as __experimentalTreeControl, Item as TreeItemType, } from './experimental-tree-control'; -export { SelectTree as __experimentalSelectTreeControl } from './experimental-select-tree-control'; +export { + SelectTree as __experimentalSelectTreeControl, + SelectTreeMenuSlot as __experimentalSelectTreeMenuSlot, +} from './experimental-select-tree-control'; export { default as TreeSelectControl } from './tree-select-control'; // Exports below can be removed once the @woocommerce/product-editor package is released. diff --git a/packages/js/product-editor/changelog/update-select_tree_dropdown b/packages/js/product-editor/changelog/update-select_tree_dropdown new file mode 100644 index 00000000000..e759cafaa65 --- /dev/null +++ b/packages/js/product-editor/changelog/update-select_tree_dropdown @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix issue with category parent select control clearing search value when typing. diff --git a/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx b/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx index 0b9d2ae3b31..eefec178245 100644 --- a/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx +++ b/packages/js/product-editor/src/components/details-categories-field/create-category-modal.tsx @@ -32,6 +32,15 @@ type CreateCategoryModalProps = { onCreate: ( newCategory: ProductCategory ) => void; }; +function getCategoryItemLabel( item: ProductCategoryNode | null ): string { + return item?.name || ''; +} +function getCategoryItemValue( + item: ProductCategoryNode | null +): string | number { + return item?.id || ''; +} + export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( { initialCategoryName, onCancel, @@ -109,8 +118,8 @@ export const CreateCategoryModal: React.FC< CreateCategoryModalProps > = ( { onRemove={ () => setCategoryParent( null ) } onInputChange={ debouncedSearch } getFilteredItems={ getFilteredItems } - getItemLabel={ ( item ) => item?.name || '' } - getItemValue={ ( item ) => item?.id || '' } + getItemLabel={ getCategoryItemLabel } + getItemValue={ getCategoryItemValue } > { ( { items, From dfdc2d3d8ccecabeff7b97436e8f61014dff9692 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Fri, 14 Apr 2023 14:24:09 +0530 Subject: [PATCH 31/33] Add null protection. --- .../src/Database/Migrations/MetaToCustomTableMigrator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php index 1401be4816a..55c3e9d1973 100644 --- a/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php +++ b/plugins/woocommerce/src/Database/Migrations/MetaToCustomTableMigrator.php @@ -718,7 +718,7 @@ WHERE $where_clause } foreach ( $results as $index => $result_row ) { $source_id = $result_row[ $this->schema_config['source']['entity']['table_name'] . '_' . $this->schema_config['source']['entity']['primary_key'] ]; - $results[ $index ] = array_merge( $result_row, $source_metadata_rows[ $source_id ] ); + $results[ $index ] = array_merge( $result_row, ( $source_metadata_rows[ $source_id ] ?? array() ) ); } return $results; } From d1ae3a5b44d6635ea4c7fc0ebc6e2ac6b0ee7d8e Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Fri, 14 Apr 2023 10:43:36 -0400 Subject: [PATCH 32/33] Exclude empty attributes in count for wcadmin_product_update Tracks event (#37718) --- .../changelog/fix-empty-attributes-tracking | 4 ++++ .../tracks/events/class-wc-products-tracking.php | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 plugins/woocommerce/changelog/fix-empty-attributes-tracking diff --git a/plugins/woocommerce/changelog/fix-empty-attributes-tracking b/plugins/woocommerce/changelog/fix-empty-attributes-tracking new file mode 100644 index 00000000000..ecbc77d5177 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-empty-attributes-tracking @@ -0,0 +1,4 @@ +Significance: patch +Type: tweak + +Exclude empty attributes from the attribute count when tracking product updates. diff --git a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php index 920f2ae1e8b..48d8fecffd6 100644 --- a/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php +++ b/plugins/woocommerce/includes/tracks/events/class-wc-products-tracking.php @@ -162,8 +162,19 @@ class WC_Products_Tracking { description_value = $( '.block-editor-rich-text__editable' ).text(); } + // We can't just check the number of '.woocommerce_attribute' elements because + // there might be empty ones, which get stripped out when saved. So, we'll check + // whether the name and values have been filled out. + var numberOfAttributes = $( '.woocommerce_attribute' ).filter( function () { + var attributeElement = $( this ); + var attributeName = attributeElement.find( 'input.attribute_name' ).val(); + var attributeValues = attributeElement.find( 'textarea[name^=\"attribute_values\"]' ).val(); + + return attributeName !== '' && attributeValues !== ''; + } ).length; + var properties = { - attributes: $( '.woocommerce_attribute' ).length, + attributes: numberOfAttributes, categories: $( '[name=\"tax_input[product_cat][]\"]:checked' ).length, cross_sells: $( '#crosssell_ids option' ).length ? 'Yes' : 'No', description: description_value.trim() !== '' ? 'Yes' : 'No', From 83458a1dee5f378c96770b1fe9079f4657465cec Mon Sep 17 00:00:00 2001 From: Joel Thiessen <444632+joelclimbsthings@users.noreply.github.com> Date: Fri, 14 Apr 2023 08:39:53 -0700 Subject: [PATCH 33/33] Adding inventory advanced section with radio and text fields (#37646) --- .../changelog/add-inventory-advanced-37401 | 4 + .../src/blocks/checkbox/block.json | 35 ++++++++ .../src/blocks/checkbox/edit.tsx | 62 ++++++++++++++ .../src/blocks/checkbox/editor.scss | 24 ++++++ .../src/blocks/checkbox/index.ts | 17 ++++ .../src/blocks/conditional/block.json | 27 ++++++ .../src/blocks/conditional/edit.tsx | 52 ++++++++++++ .../src/blocks/conditional/index.ts | 18 ++++ .../src/blocks/inventory-email/block.json | 25 ++++++ .../src/blocks/inventory-email/edit.tsx | 85 +++++++++++++++++++ .../src/blocks/inventory-email/index.ts | 22 +++++ .../src/blocks/inventory-email/style.scss | 3 + .../src/components/editor/init-blocks.ts | 6 ++ .../src/components/radio/editor.scss | 3 +- packages/js/product-editor/src/style.scss | 2 + .../changelog/add-inventory-advanced-37401 | 4 + .../includes/class-wc-post-types.php | 67 +++++++++++++++ 17 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 packages/js/product-editor/changelog/add-inventory-advanced-37401 create mode 100644 packages/js/product-editor/src/blocks/checkbox/block.json create mode 100644 packages/js/product-editor/src/blocks/checkbox/edit.tsx create mode 100644 packages/js/product-editor/src/blocks/checkbox/editor.scss create mode 100644 packages/js/product-editor/src/blocks/checkbox/index.ts create mode 100644 packages/js/product-editor/src/blocks/conditional/block.json create mode 100644 packages/js/product-editor/src/blocks/conditional/edit.tsx create mode 100644 packages/js/product-editor/src/blocks/conditional/index.ts create mode 100644 packages/js/product-editor/src/blocks/inventory-email/block.json create mode 100644 packages/js/product-editor/src/blocks/inventory-email/edit.tsx create mode 100644 packages/js/product-editor/src/blocks/inventory-email/index.ts create mode 100644 packages/js/product-editor/src/blocks/inventory-email/style.scss create mode 100644 plugins/woocommerce/changelog/add-inventory-advanced-37401 diff --git a/packages/js/product-editor/changelog/add-inventory-advanced-37401 b/packages/js/product-editor/changelog/add-inventory-advanced-37401 new file mode 100644 index 00000000000..9465dcb1bb7 --- /dev/null +++ b/packages/js/product-editor/changelog/add-inventory-advanced-37401 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding inventory email, conditional and checkbox blocks. diff --git a/packages/js/product-editor/src/blocks/checkbox/block.json b/packages/js/product-editor/src/blocks/checkbox/block.json new file mode 100644 index 00000000000..08b0348a07b --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/block.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-checkbox", + "title": "Product checkbox control", + "category": "woocommerce", + "description": "The product checkbox.", + "keywords": [ "products", "checkbox", "input" ], + "textdomain": "default", + "attributes": { + "title": { + "type": "string", + "__experimentalRole": "content" + }, + "label": { + "type": "string" + }, + "property": { + "type": "string" + }, + "tooltip": { + "type": "string" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false, + "__experimentalToolbar": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/checkbox/edit.tsx b/packages/js/product-editor/src/blocks/checkbox/edit.tsx new file mode 100644 index 00000000000..64655ac5d89 --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/edit.tsx @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { createElement, createInterpolateElement } from '@wordpress/element'; +import type { BlockAttributes } from '@wordpress/blocks'; +import { CheckboxControl, Tooltip } from '@wordpress/components'; +import { useBlockProps } from '@wordpress/block-editor'; +import { useEntityProp } from '@wordpress/core-data'; +import { Icon, help } from '@wordpress/icons'; + +/** + * Internal dependencies + */ + +export function Edit( { attributes }: { attributes: BlockAttributes } ) { + const blockProps = useBlockProps( { + className: 'woocommerce-product-form__checkbox', + } ); + const { property, title, label, tooltip } = attributes; + const [ value, setValue ] = useEntityProp< boolean >( + 'postType', + 'product', + property + ); + + return ( +
+

{ title }

+ `, { + label: { label }, + tooltip: ( + { tooltip } } + position="top center" + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Incorrect types. + className={ + 'woocommerce-product-form__checkbox-tooltip' + } + delay={ 0 } + > + + + + + ), + } ) + : label + } + checked={ value } + onChange={ ( selected ) => setValue( selected ) } + /> +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/checkbox/editor.scss b/packages/js/product-editor/src/blocks/checkbox/editor.scss new file mode 100644 index 00000000000..472ffb521b5 --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/editor.scss @@ -0,0 +1,24 @@ + +.woocommerce-product-form__checkbox { + + .components-base-control__field { + display: flex; + } + + .components-checkbox-control__label { + display: flex; + } + + &-tooltip-icon { + margin: -2px 0 0 $gap-small; + } +} + +.woocommerce-product-form__checkbox-tooltip { + .components-popover__content { + width: 200px; + min-width: auto; + white-space: normal !important; + } +} + diff --git a/packages/js/product-editor/src/blocks/checkbox/index.ts b/packages/js/product-editor/src/blocks/checkbox/index.ts new file mode 100644 index 00000000000..15144b2b28d --- /dev/null +++ b/packages/js/product-editor/src/blocks/checkbox/index.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import initBlock from '../../utils/init-block'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/js/product-editor/src/blocks/conditional/block.json b/packages/js/product-editor/src/blocks/conditional/block.json new file mode 100644 index 00000000000..572a2d90c35 --- /dev/null +++ b/packages/js/product-editor/src/blocks/conditional/block.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/conditional", + "title": "Conditional", + "category": "widgets", + "description": "Container to only conditionally render inner blocks.", + "textdomain": "default", + "attributes": { + "mustMatch": { + "__experimentalRole": "content", + "type": "array", + "items": { + "type": "object" + }, + "default": [] + } + }, + "supports": { + "align": false, + "html": false, + "multiple": true, + "reusable": false, + "inserter": false, + "lock": false + } +} diff --git a/packages/js/product-editor/src/blocks/conditional/edit.tsx b/packages/js/product-editor/src/blocks/conditional/edit.tsx new file mode 100644 index 00000000000..61e15444d4d --- /dev/null +++ b/packages/js/product-editor/src/blocks/conditional/edit.tsx @@ -0,0 +1,52 @@ +/** + * External dependencies + */ +import type { BlockAttributes } from '@wordpress/blocks'; +import { createElement, useMemo } from '@wordpress/element'; +import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { DisplayState } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityId } from '@wordpress/core-data'; + +export function Edit( { + attributes, +}: { + attributes: BlockAttributes & { + mustMatch: Record< string, Array< string > >; + }; +} ) { + const blockProps = useBlockProps(); + const { mustMatch } = attributes; + + const productId = useEntityId( 'postType', 'product' ); + const product: Product = useSelect( ( select ) => + select( 'core' ).getEditedEntityRecord( + 'postType', + 'product', + productId + ) + ); + + const displayBlocks = useMemo( () => { + for ( const [ prop, values ] of Object.entries( mustMatch ) ) { + if ( ! values.includes( product[ prop ] ) ) { + return false; + } + } + return true; + }, [ mustMatch, product ] ); + + return ( +
+ + + +
+ ); +} diff --git a/packages/js/product-editor/src/blocks/conditional/index.ts b/packages/js/product-editor/src/blocks/conditional/index.ts new file mode 100644 index 00000000000..43b3aabdc22 --- /dev/null +++ b/packages/js/product-editor/src/blocks/conditional/index.ts @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { initBlock } from '../../utils'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + initBlock( { name, metadata: metadata as never, settings } ); diff --git a/packages/js/product-editor/src/blocks/inventory-email/block.json b/packages/js/product-editor/src/blocks/inventory-email/block.json new file mode 100644 index 00000000000..749846acb5a --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/block.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "woocommerce/product-inventory-email", + "title": "Stock level threshold", + "category": "widgets", + "description": "Stock management minimum quantity.", + "keywords": [ "products", "inventory", "email", "minimum" ], + "textdomain": "default", + "attributes": { + "name": { + "type": "string", + "__experimentalRole": "content" + } + }, + "supports": { + "align": false, + "html": false, + "multiple": false, + "reusable": false, + "inserter": false, + "lock": false + }, + "editorStyle": "file:./editor.css" +} diff --git a/packages/js/product-editor/src/blocks/inventory-email/edit.tsx b/packages/js/product-editor/src/blocks/inventory-email/edit.tsx new file mode 100644 index 00000000000..44af82442d1 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/edit.tsx @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { Link } from '@woocommerce/components'; + +import { + createElement, + Fragment, + createInterpolateElement, +} from '@wordpress/element'; +import { getSetting } from '@woocommerce/settings'; + +import { useBlockProps } from '@wordpress/block-editor'; + +import { + BaseControl, + // @ts-expect-error `__experimentalInputControl` does exist. + __experimentalInputControl as InputControl, +} from '@wordpress/components'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore No types for this exist yet. +// eslint-disable-next-line @woocommerce/dependency-group +import { useEntityProp } from '@wordpress/core-data'; + +export function Edit() { + const blockProps = useBlockProps( { + className: 'woocommerce-product-form__inventory-email', + } ); + const notifyLowStockAmount = getSetting( 'notifyLowStockAmount', 2 ); + + const [ lowStockAmount, setLowStockAmount ] = useEntityProp( + 'postType', + 'product', + 'low_stock_amount' + ); + + return ( + <> +
+
+
+ store settings.', + 'woocommerce' + ), + { + link: ( + + ), + } + ) } + > + + +
+
+
+
+ + ); +} diff --git a/packages/js/product-editor/src/blocks/inventory-email/index.ts b/packages/js/product-editor/src/blocks/inventory-email/index.ts new file mode 100644 index 00000000000..59d35906a12 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/index.ts @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import { initBlock } from '../../utils'; +import metadata from './block.json'; +import { Edit } from './edit'; + +const { name } = metadata; + +export { metadata, name }; + +export const settings = { + example: {}, + edit: Edit, +}; + +export const init = () => + initBlock( { + name, + metadata: metadata as never, + settings, + } ); diff --git a/packages/js/product-editor/src/blocks/inventory-email/style.scss b/packages/js/product-editor/src/blocks/inventory-email/style.scss new file mode 100644 index 00000000000..893e1b68016 --- /dev/null +++ b/packages/js/product-editor/src/blocks/inventory-email/style.scss @@ -0,0 +1,3 @@ +.woocommerce-product-form__inventory-email { + margin-top: $gap-large; +} diff --git a/packages/js/product-editor/src/components/editor/init-blocks.ts b/packages/js/product-editor/src/components/editor/init-blocks.ts index 8eef2638b22..346fecf1720 100644 --- a/packages/js/product-editor/src/components/editor/init-blocks.ts +++ b/packages/js/product-editor/src/components/editor/init-blocks.ts @@ -23,6 +23,9 @@ import { init as initCollapsible } from '../collapsible-block'; import { init as initScheduleSale } from '../../blocks/schedule-sale'; import { init as initTrackInventory } from '../../blocks/track-inventory'; import { init as initSku } from '../../blocks/inventory-sku'; +import { init as initConditional } from '../../blocks/conditional'; +import { init as initLowStockQty } from '../../blocks/inventory-email'; +import { init as initCheckbox } from '../../blocks/checkbox'; export const initBlocks = () => { const coreBlocks = __experimentalGetCoreBlocks(); @@ -44,4 +47,7 @@ export const initBlocks = () => { initScheduleSale(); initTrackInventory(); initSku(); + initConditional(); + initLowStockQty(); + initCheckbox(); }; diff --git a/packages/js/product-editor/src/components/radio/editor.scss b/packages/js/product-editor/src/components/radio/editor.scss index 8b1729d3d81..185c801248e 100644 --- a/packages/js/product-editor/src/components/radio/editor.scss +++ b/packages/js/product-editor/src/components/radio/editor.scss @@ -13,7 +13,6 @@ font-size: 16px; font-weight: 600; color: #1e1e1e; - margin-bottom: $gap-smaller; } &__description { @@ -24,4 +23,4 @@ .components-base-control__field > .components-v-stack { gap: $gap; } -} \ No newline at end of file +} diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss index e263448c1bc..825a8d367fe 100644 --- a/packages/js/product-editor/src/style.scss +++ b/packages/js/product-editor/src/style.scss @@ -9,6 +9,7 @@ @import 'components/images/editor.scss'; @import 'components/block-editor/style.scss'; @import 'components/radio/editor.scss'; +@import 'blocks/checkbox/editor.scss'; @import 'components/section/editor.scss'; @import 'components/tab/editor.scss'; @import 'components/tabs/style.scss'; @@ -17,3 +18,4 @@ @import 'components/product-mvp-feedback-modal/style.scss'; @import 'components/details-name-block/style.scss'; @import 'blocks/inventory-sku/style.scss'; +@import 'blocks/inventory-email/style.scss'; diff --git a/plugins/woocommerce/changelog/add-inventory-advanced-37401 b/plugins/woocommerce/changelog/add-inventory-advanced-37401 new file mode 100644 index 00000000000..6bbe1d6982d --- /dev/null +++ b/plugins/woocommerce/changelog/add-inventory-advanced-37401 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding checkbox, conditional and inventory email blocks to product blocks editor. diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php index c419c9ba45c..5670bf77b90 100644 --- a/plugins/woocommerce/includes/class-wc-post-types.php +++ b/plugins/woocommerce/includes/class-wc-post-types.php @@ -605,6 +605,73 @@ class WC_Post_Types { array( 'woocommerce/product-track-inventory-fields', ), + array( + 'woocommerce/collapsible', + array( + 'toggleText' => __( 'Advanced', 'woocommerce' ), + 'initialCollapsed' => true, + 'persistRender' => true, + ), + array( + array( + 'woocommerce/conditional', + array( + 'mustMatch' => array( + 'manage_stock' => array( true ), + ), + ), + array( + array( + 'woocommerce/product-radio', + array( + 'title' => __( 'When out of stock', 'woocommerce' ), + 'property' => 'backorders', + 'options' => array( + array( + 'label' => __( 'Allow purchases', 'woocommerce' ), + 'value' => 'yes', + ), + array( + 'label' => __( + 'Allow purchases, but notify customers', + 'woocommerce' + ), + 'value' => 'notify', + ), + array( + 'label' => __( "Don't allow purchases", 'woocommerce' ), + 'value' => 'no', + ), + ), + ), + ), + array( + 'woocommerce/product-inventory-email', + ), + ), + ), + array( + 'woocommerce/product-checkbox', + array( + 'title' => __( + 'Restrictions', + 'woocommerce' + ), + 'label' => __( + 'Limit purchases to 1 item per order', + 'woocommerce' + ), + 'property' => 'sold_individually', + 'tooltip' => __( + 'When checked, customers will be able to purchase only 1 item in a single order. This is particularly useful for items that have limited quantity, like art or handmade goods.', + 'woocommerce' + ), + ), + ), + + ), + ), + ), ), ),