From bddb6621eea0bc6da2e487df98a38c304a57420f Mon Sep 17 00:00:00 2001 From: "Jorge A. Torres" Date: Mon, 4 Jul 2022 20:57:01 -0300 Subject: [PATCH] [COT] Implement order bulk actions (#33687) Introduce `Admin\Orders\PageController` to manage admin pages for orders * Change orders menu registration & initialization to use `Admin\Orders\PageController` * Implement bulk actions in orders ListTable * Unnecessary import * Make PHPCS happy * Add changelog * Address feedback --- .../add-33198-cot-admin-list-table-actions | 4 + .../includes/admin/class-wc-admin-menus.php | 34 +--- .../src/Internal/Admin/Orders/ListTable.php | 155 +++++++++++++++++- .../Internal/Admin/Orders/PageController.php | 116 +++++++++++++ 4 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 plugins/woocommerce/changelog/add-33198-cot-admin-list-table-actions create mode 100644 plugins/woocommerce/src/Internal/Admin/Orders/PageController.php diff --git a/plugins/woocommerce/changelog/add-33198-cot-admin-list-table-actions b/plugins/woocommerce/changelog/add-33198-cot-admin-list-table-actions new file mode 100644 index 00000000000..f2703e6e271 --- /dev/null +++ b/plugins/woocommerce/changelog/add-33198-cot-admin-list-table-actions @@ -0,0 +1,4 @@ +Significance: patch +Type: update + +Implement bulk actions in the new orders admin list table. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php index 99671c3e226..3205076eff3 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -7,6 +7,7 @@ */ use Automattic\WooCommerce\Internal\Admin\Orders\ListTable as Custom_Orders_List_Table; +use Automattic\WooCommerce\Internal\Admin\Orders\PageController as Custom_Orders_PageController; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; defined( 'ABSPATH' ) || exit; @@ -316,40 +317,11 @@ class WC_Admin_Menus { */ public function orders_menu(): void { if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { - add_submenu_page( 'woocommerce', __( 'Orders', 'woocommerce' ), __( 'Orders', 'woocommerce' ), 'edit_others_shop_orders', 'wc-orders', array( $this, 'orders_page' ) ); - add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'orders_table' ) ); - - // In some cases (such as if the authoritative order store was changed earlier in the current request) we - // need an extra step to remove the menu entry for the menu post type. - add_action( - 'admin_init', - function () { - remove_submenu_page( 'woocommerce', 'edit.php?post_type=shop_order' ); - } - ); + $this->orders_page_controller = new Custom_Orders_PageController(); + $this->orders_page_controller->setup(); } } - /** - * Set-up the orders admin list table. - * - * @return void - */ - public function orders_table(): void { - $this->orders_list_table = new Custom_Orders_List_Table(); - $this->orders_list_table->setup(); - } - - /** - * Render the orders admin list table. - * - * @return void - */ - public function orders_page(): void { - $this->orders_list_table->prepare_items(); - $this->orders_list_table->display(); - } - /** * Add custom nav meta box. * diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php index 2317ed68ef3..d23373236a0 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/ListTable.php @@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use WC_Order; -use WC_Order_Data_Store_Interface; use WP_List_Table; use WP_Screen; @@ -33,9 +32,12 @@ class ListTable extends WP_List_Table { * @return void */ public function setup(): void { + add_action( 'admin_notices', array( $this, 'bulk_action_notices' ) ); + add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'get_columns' ) ); add_filter( 'set_screen_option_edit_orders_per_page', array( $this, 'set_items_per_page' ), 10, 3 ); add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ), 10, 2 ); + $this->items_per_page(); set_screen_options(); } @@ -118,7 +120,7 @@ class ListTable extends WP_List_Table { /** * Renders after the 'blank state' message for the order list table has rendered. */ - do_action( 'wc_marketplace_suggestions_orders_empty_state' ); + do_action( 'wc_marketplace_suggestions_orders_empty_state' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment ?> @@ -458,7 +460,7 @@ class ListTable extends WP_List_Table { human_time_diff( $order->get_date_created()->getTimestamp(), time() ) ); } else { - $show_date = $order->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); + $show_date = $order->get_date_created()->date_i18n( apply_filters( 'woocommerce_admin_order_date_format', __( 'M j, Y', 'woocommerce' ) ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment } printf( '', @@ -583,6 +585,7 @@ class ListTable extends WP_List_Table { * are registered. * * @param WC_Order $order Current order object. + * @since 6.7.0 */ do_action( 'woocommerce_admin_order_actions_start', $order ); @@ -609,6 +612,7 @@ class ListTable extends WP_List_Table { * * @param array $action Order actions. * @param WC_Order $order Current order object. + * @since 6.7.0 */ $actions = apply_filters( 'woocommerce_admin_order_actions', $actions, $order ); @@ -620,6 +624,7 @@ class ListTable extends WP_List_Table { * are rendered. * * @param WC_Order $order Current order object. + * @since 6.7.0 */ do_action( 'woocommerce_admin_order_actions_end', $order ); @@ -649,4 +654,148 @@ class ListTable extends WP_List_Table { echo ''; } } + + /** + * Handle bulk actions. + */ + public function handle_bulk_actions() { + $action = $this->current_action(); + + if ( ! $action ) { + return; + } + + check_admin_referer( 'bulk-orders' ); + + $redirect_to = remove_query_arg( array( 'deleted', 'ids' ), wp_get_referer() ); + $redirect_to = add_query_arg( 'paged', $this->get_pagenum(), $redirect_to ); + + /** + * Allows 3rd parties to modify order IDs about to be affected by a bulk action. + * + * @param array Array of order IDs. + */ + $ids = apply_filters( // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment + 'woocommerce_bulk_action_ids', + isset( $_REQUEST['order'] ) ? array_reverse( array_map( 'absint', $_REQUEST['order'] ) ) : array(), + $action, + 'order' + ); + + if ( ! $ids ) { + wp_safe_redirect( $redirect_to ); + exit; + } + + $report_action = ''; + $changed = 0; + + if ( 'remove_personal_data' === $action ) { + $report_action = 'removed_personal_data'; + $changed = $this->do_bulk_action_remove_personal_data( $ids ); + } elseif ( false !== strpos( $action, 'mark_' ) ) { + $order_statuses = wc_get_order_statuses(); + $new_status = substr( $action, 5 ); + $report_action = 'marked_' . $new_status; + + if ( isset( $order_statuses[ 'wc-' . $new_status ] ) ) { + $changed = $this->do_bulk_action_mark_orders( $ids, $new_status ); + } + } + + if ( $changed ) { + $redirect_to = add_query_arg( + array( + 'bulk_action' => $report_action, + 'changed' => $changed, + 'ids' => implode( ',', $ids ), + ), + $redirect_to + ); + } + + wp_safe_redirect( $redirect_to ); + exit; + } + + /** + * Implements the "remove personal data" bulk action. + * + * @param array $order_ids The Order IDs. + * @return int Number of orders modified. + */ + private function do_bulk_action_remove_personal_data( $order_ids ): int { + $changed = 0; + + foreach ( $order_ids as $id ) { + $order = wc_get_order( $id ); + + if ( ! $order ) { + continue; + } + + do_action( 'woocommerce_remove_order_personal_data', $order ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $changed++; + } + + return $changed; + } + + /** + * Implements the "mark " bulk action. + * + * @param array $order_ids The order IDs to change. + * @param string $new_status The new order status. + * @return int Number of orders modified. + */ + private function do_bulk_action_mark_orders( $order_ids, $new_status ): int { + $changed = 0; + + // Initialize payment gateways in case order has hooked status transition actions. + WC()->payment_gateways(); + + foreach ( $order_ids as $id ) { + $order = wc_get_order( $id ); + + if ( ! $order ) { + continue; + } + + $order->update_status( $new_status, __( 'Order status changed by bulk edit.', 'woocommerce' ), true ); + do_action( 'woocommerce_order_edit_status', $id, $new_status ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $changed++; + } + + return $changed; + } + + /** + * Show confirmation message that order status changed for number of orders. + */ + public function bulk_action_notices() { + if ( empty( $_REQUEST['bulk_action'] ) ) { + return; + } + + $order_statuses = wc_get_order_statuses(); + $number = absint( $_REQUEST['changed'] ?? 0 ); + $bulk_action = wc_clean( wp_unslash( $_REQUEST['bulk_action'] ) ); + + // Check if any status changes happened. + foreach ( $order_statuses as $slug => $name ) { + if ( 'marked_' . str_replace( 'wc-', '', $slug ) === $bulk_action ) { // WPCS: input var ok, CSRF ok. + /* translators: %s: orders count */ + $message = sprintf( _n( '%s order status changed.', '%s order statuses changed.', $number, 'woocommerce' ), number_format_i18n( $number ) ); + echo '

' . esc_html( $message ) . '

'; + break; + } + } + + if ( 'removed_personal_data' === $bulk_action ) { // WPCS: input var ok, CSRF ok. + /* translators: %s: orders count */ + $message = sprintf( _n( 'Removed personal data from %s order.', 'Removed personal data from %s orders.', $number, 'woocommerce' ), number_format_i18n( $number ) ); + echo '

' . esc_html( $message ) . '

'; + } + } + } diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php new file mode 100644 index 00000000000..1f0c45336c1 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/PageController.php @@ -0,0 +1,116 @@ +register_menu(); + } else { + add_action( 'admin_menu', 'register_menu', 9 ); + } + + $this->set_action(); + + // Perform initialization for the current action. + add_action( + 'load-woocommerce_page_wc-orders', + function() { + if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) { + $this->{"setup_action_{$this->current_action}"}(); + } + } + ); + } + + /** + * Sets the current action based on querystring arguments. Defaults to 'list_orders'. + * + * @return void + */ + private function set_action(): void { + $this->current_action = 'list_orders'; + + if ( ! empty( $_GET['action'] ) && 'edit' === $_GET['action'] ) { + $this->current_action = 'edit_order'; + } + } + + /** + * Registers the "Orders" menu. + * + * @return void + */ + public function register_menu(): void { + add_submenu_page( + 'woocommerce', + __( 'Orders', 'woocommerce' ), + __( 'Orders', 'woocommerce' ), + 'edit_others_shop_orders', + 'wc-orders', + array( $this, 'output' ) + ); + + // In some cases (such as if the authoritative order store was changed earlier in the current request) we + // need an extra step to remove the menu entry for the menu post type. + add_action( + 'admin_init', + function() { + remove_submenu_page( 'woocommerce', 'edit.php?post_type=shop_order' ); + } + ); + } + + /** + * Outputs content for the current orders screen. + * + * @return void + */ + public function output(): void { + switch ( $this->current_action ) { + case 'list_orders': + default: + $this->orders_table->prepare_items(); + $this->orders_table->display(); + break; + } + } + + /** + * Handles initialization of the orders list table. + * + * @return void + */ + private function setup_action_list_orders(): void { + $this->orders_table = new ListTable(); + $this->orders_table->setup(); + + if ( $this->orders_table->current_action() ) { + $this->orders_table->handle_bulk_actions(); + } + } + +}