From 3d117e1ac97e7f30afb8d60def7f4ff36ad71837 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 5 Jul 2023 17:13:41 +0100 Subject: [PATCH] Order confirmation: Convert Order Details Templates to Blocks (#10095) * Move code from templates into the details block * Details -> Totals * Downloads block * Sample content for downloads block * Add block icon --- .../{details => downloads}/block.json | 6 +- .../order-confirmation/downloads/edit.tsx | 43 ++++ .../order-confirmation/downloads/index.tsx | 30 +++ .../order-confirmation/downloads/style.scss | 27 +++ .../order-confirmation/totals/block.json | 29 +++ .../{details => totals}/edit.tsx | 11 +- .../{details => totals}/index.tsx | 0 .../{details => totals}/style.scss | 6 +- bin/webpack-entries.js | 7 +- src/BlockTypes/OrderConfirmation/Details.php | 158 ------------ .../OrderConfirmation/Downloads.php | 167 +++++++++++++ src/BlockTypes/OrderConfirmation/Totals.php | 226 ++++++++++++++++++ src/BlockTypesController.php | 3 +- 13 files changed, 547 insertions(+), 166 deletions(-) rename assets/js/blocks/order-confirmation/{details => downloads}/block.json (76%) create mode 100644 assets/js/blocks/order-confirmation/downloads/edit.tsx create mode 100644 assets/js/blocks/order-confirmation/downloads/index.tsx create mode 100644 assets/js/blocks/order-confirmation/downloads/style.scss create mode 100644 assets/js/blocks/order-confirmation/totals/block.json rename assets/js/blocks/order-confirmation/{details => totals}/edit.tsx (76%) rename assets/js/blocks/order-confirmation/{details => totals}/index.tsx (100%) rename assets/js/blocks/order-confirmation/{details => totals}/style.scss (83%) delete mode 100644 src/BlockTypes/OrderConfirmation/Details.php create mode 100644 src/BlockTypes/OrderConfirmation/Downloads.php create mode 100644 src/BlockTypes/OrderConfirmation/Totals.php diff --git a/assets/js/blocks/order-confirmation/details/block.json b/assets/js/blocks/order-confirmation/downloads/block.json similarity index 76% rename from assets/js/blocks/order-confirmation/details/block.json rename to assets/js/blocks/order-confirmation/downloads/block.json index c52543256ab..f1d30143468 100644 --- a/assets/js/blocks/order-confirmation/details/block.json +++ b/assets/js/blocks/order-confirmation/downloads/block.json @@ -1,8 +1,8 @@ { - "name": "woocommerce/order-confirmation-details", + "name": "woocommerce/order-confirmation-downloads", "version": "1.0.0", - "title": "Order Confirmation Details", - "description": "Display the order confirmation details.", + "title": "Order Downloads", + "description": "Display links to purchased downloads.", "category": "woocommerce", "keywords": [ "WooCommerce" ], "supports": { diff --git a/assets/js/blocks/order-confirmation/downloads/edit.tsx b/assets/js/blocks/order-confirmation/downloads/edit.tsx new file mode 100644 index 00000000000..5986d814abd --- /dev/null +++ b/assets/js/blocks/order-confirmation/downloads/edit.tsx @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import ServerSideRender from '@wordpress/server-side-render'; +import { useBlockProps } from '@wordpress/block-editor'; +import { Disabled } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import './style.scss'; + +interface Props { + attributes: { + align: string; + className: string; + isPreview: boolean; + }; + name: string; +} + +const Edit = ( props: Props ): JSX.Element => { + const { attributes, name } = props; + const blockProps = useBlockProps( { + className: 'wc-block-order-confirmation-downloads', + } ); + + return ( +
+ + + +
+ ); +}; + +export default Edit; diff --git a/assets/js/blocks/order-confirmation/downloads/index.tsx b/assets/js/blocks/order-confirmation/downloads/index.tsx new file mode 100644 index 00000000000..c5f312734aa --- /dev/null +++ b/assets/js/blocks/order-confirmation/downloads/index.tsx @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; +import { Icon, download } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; +import './style.scss'; + +registerBlockType( metadata, { + icon: { + src: ( + + ), + }, + attributes: { + ...metadata.attributes, + }, + edit, + save() { + return null; + }, +} ); diff --git a/assets/js/blocks/order-confirmation/downloads/style.scss b/assets/js/blocks/order-confirmation/downloads/style.scss new file mode 100644 index 00000000000..00802bfd384 --- /dev/null +++ b/assets/js/blocks/order-confirmation/downloads/style.scss @@ -0,0 +1,27 @@ +.wc-block-order-confirmation-downloads { + .woocommerce-order-downloads table, + .woocommerce-order-downloads table.shop_table { + width: 100%; + border: 1px solid currentColor; + border-bottom: 0; + + thead { + text-transform: uppercase; + } + + th { + font-weight: bold; + } + + th, + td { + border-style: solid; + border-color: currentColor; + border-width: 0 0 1px 0; + padding: $gap-small; + margin: 0; + text-align: left; + } + } +} + diff --git a/assets/js/blocks/order-confirmation/totals/block.json b/assets/js/blocks/order-confirmation/totals/block.json new file mode 100644 index 00000000000..1813ce6de21 --- /dev/null +++ b/assets/js/blocks/order-confirmation/totals/block.json @@ -0,0 +1,29 @@ +{ + "name": "woocommerce/order-confirmation-totals", + "version": "1.0.0", + "title": "Order Totals", + "description": "Display the items purchased and order totals.", + "category": "woocommerce", + "keywords": [ "WooCommerce" ], + "supports": { + "multiple": false, + "align": [ "wide", "full" ] + }, + "attributes": { + "align": { + "type": "string", + "default": "wide" + }, + "className": { + "type": "string", + "default": "" + }, + "isPreview": { + "type": "boolean", + "default": false + } + }, + "textdomain": "woo-gutenberg-products-block", + "apiVersion": 2, + "$schema": "https://schemas.wp.org/trunk/block.json" +} diff --git a/assets/js/blocks/order-confirmation/details/edit.tsx b/assets/js/blocks/order-confirmation/totals/edit.tsx similarity index 76% rename from assets/js/blocks/order-confirmation/details/edit.tsx rename to assets/js/blocks/order-confirmation/totals/edit.tsx index e3c63228757..9283070a185 100644 --- a/assets/js/blocks/order-confirmation/details/edit.tsx +++ b/assets/js/blocks/order-confirmation/totals/edit.tsx @@ -9,10 +9,19 @@ import { useBlockProps } from '@wordpress/block-editor'; */ import './style.scss'; +interface Props { + attributes: { + align: string; + className: string; + isPreview: boolean; + }; + name: string; +} + const Edit = ( props: Props ): JSX.Element => { const { attributes, name } = props; const blockProps = useBlockProps( { - className: 'wc-block-order-confirmation-details', + className: 'wc-block-order-confirmation-totals', } ); return ( diff --git a/assets/js/blocks/order-confirmation/details/index.tsx b/assets/js/blocks/order-confirmation/totals/index.tsx similarity index 100% rename from assets/js/blocks/order-confirmation/details/index.tsx rename to assets/js/blocks/order-confirmation/totals/index.tsx diff --git a/assets/js/blocks/order-confirmation/details/style.scss b/assets/js/blocks/order-confirmation/totals/style.scss similarity index 83% rename from assets/js/blocks/order-confirmation/details/style.scss rename to assets/js/blocks/order-confirmation/totals/style.scss index 2efafb7ef59..532e590ea38 100644 --- a/assets/js/blocks/order-confirmation/details/style.scss +++ b/assets/js/blocks/order-confirmation/totals/style.scss @@ -1,4 +1,4 @@ -.wc-block-order-confirmation-details { +.wc-block-order-confirmation-totals { .woocommerce-order-details table, .woocommerce-order-details table.shop_table { width: 100%; @@ -9,6 +9,10 @@ text-transform: uppercase; } + th { + font-weight: bold; + } + th, td { border-style: solid; diff --git a/bin/webpack-entries.js b/bin/webpack-entries.js index d2b62da6536..1da48103b58 100644 --- a/bin/webpack-entries.js +++ b/bin/webpack-entries.js @@ -93,8 +93,11 @@ const blocks = { 'order-confirmation-summary': { customDir: 'order-confirmation/summary', }, - 'order-confirmation-details': { - customDir: 'order-confirmation/details', + 'order-confirmation-totals': { + customDir: 'order-confirmation/totals', + }, + 'order-confirmation-downloads': { + customDir: 'order-confirmation/downloads', }, 'order-confirmation-billing-address': { customDir: 'order-confirmation/billing-address', diff --git a/src/BlockTypes/OrderConfirmation/Details.php b/src/BlockTypes/OrderConfirmation/Details.php deleted file mode 100644 index a5066a4a42a..00000000000 --- a/src/BlockTypes/OrderConfirmation/Details.php +++ /dev/null @@ -1,158 +0,0 @@ -get_preview_order(); - } else { - $order = $this->get_order(); - - if ( ! $this->is_current_customer_order( $order ) ) { - $order = null; - } - } - - $content = $order ? $this->render_content( $order ) : $this->render_content_fallback(); - $classname = $attributes['className'] ?? ''; - $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); - - if ( isset( $attributes['align'] ) ) { - $classname .= " align{$attributes['align']}"; - } - - return sprintf( - '
%3$s
', - esc_attr( $classes_and_styles['classes'] ), - esc_attr( $classname ), - $content, - esc_attr( $this->block_name ) - ); - } - - /** - * This is what gets rendered when the order does not exist. - * - * @return string - */ - protected function render_content_fallback() { - return '

' . esc_html__( 'The details of your order can be found in the email that was sent to you when the order was placed.', 'woo-gutenberg-products-block' ) . '

'; - } - - /** - * This renders the content of the block within the wrapper. - * - * @param \WC_Order $order Order object. - * @return string - */ - protected function render_content( $order ) { - return ' -
- ' . $this->get_hook_content( 'woocommerce_order_details_before_order_table', [ $order ] ) . ' - - - - - - - - - ' . $this->get_hook_content( 'woocommerce_order_details_before_order_table_items', [ $order ] ) . ' - ' . $this->render_order_details_table_items( $order ) . ' - ' . $this->get_hook_content( 'woocommerce_order_details_after_order_table_items', [ $order ] ) . ' - - - ' . $this->render_order_details_table_totals( $order ) . ' - -
' . esc_html__( 'Product', 'woo-gutenberg-products-block' ) . '' . esc_html__( 'Total', 'woo-gutenberg-products-block' ) . '
- ' . $this->get_hook_content( 'woocommerce_order_details_after_order_table', [ $order ] ) . ' -
- ' . $this->get_hook_content( 'woocommerce_after_order_details', [ $order ] ) . ' - '; - } - - /** - * Render order details table items. - * - * @param \WC_Order $order Order object. - * @return string - */ - protected function render_order_details_table_items( $order ) { - ob_start(); - - // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment - $show_purchase_note = $order->has_status( apply_filters( 'woocommerce_purchase_note_order_statuses', array( 'completed', 'processing' ) ) ); - - // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment - foreach ( $order->get_items( apply_filters( 'woocommerce_purchase_order_item_types', 'line_item' ) ) as $item_id => $item ) { - $product = $item->get_product(); - - wc_get_template( - 'order/order-details-item.php', - array( - 'order' => $order, - 'item_id' => $item_id, - 'item' => $item, - 'show_purchase_note' => $show_purchase_note, - 'purchase_note' => $product ? $product->get_purchase_note() : '', - 'product' => $product, - ) - ); - } - - return ob_get_clean(); - } - - /** - * Render order details table totals. - * - * @param \WC_Order $order Order object. - * @return string - */ - protected function render_order_details_table_totals( $order ) { - ob_start(); - - foreach ( $order->get_order_item_totals() as $key => $total ) { - ?> - - - - - get_customer_note() ) { - ?> - - - get_customer_note() ) ) ); ?> - - get_preview_order(); + $show_downloads = true; + $downloads = [ + [ + 'product_name' => 'Test Product', + 'product_url' => 'https://example.com', + 'download_name' => 'Test Download', + 'download_url' => 'https://example.com', + ], + ]; + } else { + $order = $this->get_order(); + + if ( ! $this->is_current_customer_order( $order ) ) { + $order = null; + } + + $show_downloads = $order && $order->has_downloadable_item() && $order->is_download_permitted(); + $downloads = $order ? $order->get_downloadable_items() : []; + } + + $content = $order && $show_downloads ? $this->render_content( $order, $downloads ) : $this->render_content_fallback(); + $classname = $attributes['className'] ?? ''; + $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); + + if ( isset( $attributes['align'] ) ) { + $classname .= " align{$attributes['align']}"; + } + + return $content ? sprintf( + '
%3$s
', + esc_attr( $classes_and_styles['classes'] ), + esc_attr( $classname ), + $content, + esc_attr( $this->block_name ) + ) : ''; + } + + /** + * This renders the content of the block within the wrapper. + * + * @param \WC_Order $order Order object. + * @param array $downloads Array of downloads. + * @return string + */ + protected function render_content( $order, $downloads = [] ) { + return ' +
+ + + + ' . $this->render_order_downloads_column_headers( $order ) . ' + + + + ' . $this->render_order_downloads( $order, $downloads ) . ' + +
+
+ '; + } + + /** + * Render column headers for downloads table. + * + * @return string + */ + protected function render_order_downloads_column_headers() { + $columns = wc_get_account_downloads_columns(); + $return = ''; + + foreach ( $columns as $column_id => $column_name ) { + $return .= '' . esc_html( $column_name ) . ''; + } + + return $return; + } + + /** + * Render downloads. + * + * @param \WC_Order $order Order object. + * @param array $downloads Array of downloads. + * @return string + */ + protected function render_order_downloads( $order, $downloads ) { + $return = ''; + foreach ( $downloads as $download ) { + $return .= '' . $this->render_order_download_row( $download ) . ''; + } + return $return; + } + + /** + * Render a download row in the table. + * + * @param array $download Download data. + * @return string + */ + protected function render_order_download_row( $download ) { + $return = ''; + + foreach ( wc_get_account_downloads_columns() as $column_id => $column_name ) { + $return .= ''; + + if ( has_action( 'woocommerce_account_downloads_column_' . $column_id ) ) { + $return .= $this->get_hook_content( 'woocommerce_account_downloads_column_' . $column_id, [ $download ] ); + } else { + switch ( $column_id ) { + case 'download-product': + if ( $download['product_url'] ) { + $return .= '' . esc_html( $download['product_name'] ) . ''; + } else { + $return .= esc_html( $download['product_name'] ); + } + break; + case 'download-file': + $return .= '' . esc_html( $download['download_name'] ) . ''; + break; + case 'download-remaining': + $return .= is_numeric( $download['downloads_remaining'] ) ? esc_html( $download['downloads_remaining'] ) : esc_html__( '∞', 'woo-gutenberg-products-block' ); + break; + case 'download-expires': + if ( ! empty( $download['access_expires'] ) ) { + $return .= ''; + } else { + $return .= esc_html__( 'Never', 'woo-gutenberg-products-block' ); + } + break; + } + } + + $return .= ''; + } + + return $return; + } +} diff --git a/src/BlockTypes/OrderConfirmation/Totals.php b/src/BlockTypes/OrderConfirmation/Totals.php new file mode 100644 index 00000000000..40fcdbe5045 --- /dev/null +++ b/src/BlockTypes/OrderConfirmation/Totals.php @@ -0,0 +1,226 @@ +get_preview_order(); + } else { + $order = $this->get_order(); + + if ( ! $this->is_current_customer_order( $order ) ) { + $order = null; + } + } + + $content = $order ? $this->render_content( $order ) : $this->render_content_fallback(); + $classname = $attributes['className'] ?? ''; + $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes ); + + if ( isset( $attributes['align'] ) ) { + $classname .= " align{$attributes['align']}"; + } + + return sprintf( + '
%3$s
', + esc_attr( $classes_and_styles['classes'] ), + esc_attr( $classname ), + $content, + esc_attr( $this->block_name ) + ); + } + + /** + * This is what gets rendered when the order does not exist. + * + * @return string + */ + protected function render_content_fallback() { + return '

' . esc_html__( 'The details of your order can be found in the email that was sent to you when the order was placed.', 'woo-gutenberg-products-block' ) . '

'; + } + + /** + * This renders the content of the block within the wrapper. + * + * @param \WC_Order $order Order object. + * @return string + */ + protected function render_content( $order ) { + return ' +
+ ' . $this->get_hook_content( 'woocommerce_order_details_before_order_table', [ $order ] ) . ' + + + + + + + + + ' . $this->get_hook_content( 'woocommerce_order_details_before_order_table_items', [ $order ] ) . ' + ' . $this->render_order_details_table_items( $order ) . ' + ' . $this->get_hook_content( 'woocommerce_order_details_after_order_table_items', [ $order ] ) . ' + + + ' . $this->render_order_details_table_totals( $order ) . ' + ' . $this->render_order_details_table_customer_note( $order ) . ' + +
' . esc_html__( 'Product', 'woo-gutenberg-products-block' ) . '' . esc_html__( 'Total', 'woo-gutenberg-products-block' ) . '
+ ' . $this->get_hook_content( 'woocommerce_order_details_after_order_table', [ $order ] ) . ' +
+ ' . $this->get_hook_content( 'woocommerce_after_order_details', [ $order ] ) . ' + '; + } + + /** + * Render order details table items. + * + * Loosely based on the templates order-details.php and order-details-item.php from core. + * + * @param \WC_Order $order Order object. + * @return string + */ + protected function render_order_details_table_items( $order ) { + $return = ''; + $order_items = array_filter( + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $order->get_items( apply_filters( 'woocommerce_purchase_order_item_types', 'line_item' ) ), + function( $item ) { + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + return apply_filters( 'woocommerce_order_item_visible', true, $item ); + } + ); + + foreach ( $order_items as $item_id => $item ) { + $product = $item->get_product(); + $return .= $this->render_order_details_table_item( $order, $item_id, $item, $product ); + } + + return $return; + } + + /** + * Render an item in the order details table. + * + * @param \WC_Order $order Order object. + * @param integer $item_id Item ID. + * @param \WC_Order_Item $item Item object. + * @param \WC_Product|false $product Product object if it exists. + * @return string + */ + protected function render_order_details_table_item( $order, $item_id, $item, $product ) { + $is_visible = $product && $product->is_visible(); + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $row_class = apply_filters( 'woocommerce_order_item_class', 'woocommerce-table__line-item order_item', $item, $order ); + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $product_permalink = apply_filters( 'woocommerce_order_item_permalink', $is_visible ? $product->get_permalink( $item ) : '', $item, $order ); + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $item_name = apply_filters( + 'woocommerce_order_item_name', + $product_permalink ? sprintf( '%s', $product_permalink, $item->get_name() ) : $item->get_name(), + $item, + $is_visible + ); + $qty = $item->get_quantity(); + $refunded_qty = $order->get_qty_refunded_for_item( $item_id ); + $qty_display = $refunded_qty ? '' . esc_html( $qty ) . ' ' . esc_html( $qty - ( $refunded_qty * -1 ) ) . '' : esc_html( $qty ); + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $item_qty = apply_filters( + 'woocommerce_order_item_quantity_html', + '' . sprintf( '× %s', $qty_display ) . '', + $item + ); + + return ' + + + ' . wp_kses_post( $item_name ) . '  + ' . wp_kses_post( $item_qty ) . ' + ' . $this->get_hook_content( 'woocommerce_order_item_meta_start', [ $item_id, $item, $order, false ] ) . ' + ' . wc_display_item_meta( $item, [ 'echo' => false ] ) . ' + ' . $this->get_hook_content( 'woocommerce_order_item_meta_end', [ $item_id, $item, $order, false ] ) . ' + ' . $this->render_order_details_table_item_purchase_note( $order, $product ) . ' + + + ' . wp_kses_post( $order->get_formatted_line_subtotal( $item ) ) . ' + + + '; + } + + /** + * Render an item purchase note. + * + * @param \WC_Order $order Order object. + * @param \WC_Product|false $product Product object if it exists. + * @return string + */ + protected function render_order_details_table_item_purchase_note( $order, $product ) { + // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment + $show_purchase_note = $order->has_status( apply_filters( 'woocommerce_purchase_note_order_statuses', array( 'completed', 'processing' ) ) ); + $purchase_note = $product ? $product->get_purchase_note() : ''; + + return $show_purchase_note && $purchase_note ? '
' . wp_kses_post( $purchase_note ) . '
' : ''; + } + + /** + * Render order details table totals. + * + * @param \WC_Order $order Order object. + * @return string + */ + protected function render_order_details_table_totals( $order ) { + $return = ''; + + foreach ( $order->get_order_item_totals() as $total ) { + $return .= ' + + ' . esc_html( $total['label'] ) . ' + ' . wp_kses_post( $total['value'] ) . ' + + '; + } + + return $return; + } + + /** + * Render customer note. + * + * @param \WC_Order $order Order object. + * @return string + */ + protected function render_order_details_table_customer_note( $order ) { + if ( ! $order->get_customer_note() ) { + return ''; + } + + return ' + ' . esc_html__( 'Note:', 'woo-gutenberg-products-block' ) . ' + ' . wp_kses_post( nl2br( wptexturize( $order->get_customer_note() ) ) ) . ' + '; + } +} diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php index b8a7a7833f4..faeefe890e7 100644 --- a/src/BlockTypesController.php +++ b/src/BlockTypesController.php @@ -235,7 +235,8 @@ protected function get_block_types() { $block_types[] = 'ProductGalleryThumbnails'; $block_types[] = 'OrderConfirmation\Status'; $block_types[] = 'OrderConfirmation\Summary'; - $block_types[] = 'OrderConfirmation\Details'; + $block_types[] = 'OrderConfirmation\Totals'; + $block_types[] = 'OrderConfirmation\Downloads'; $block_types[] = 'OrderConfirmation\BillingAddress'; $block_types[] = 'OrderConfirmation\ShippingAddress'; }