From ed0d38c44b1e4df61e9c00e8e873bdbd0ab39da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maikel=20David=20P=C3=A9rez=20G=C3=B3mez?= Date: Thu, 16 Nov 2023 16:11:44 -0300 Subject: [PATCH] [External products] Product details (#41442) * Add buy button section * Enable external product support into the product block editor * Hide buy button section when product type is not external * Remove BaseControl from TextControl since it's not required anymore, InputControl takes care of that now * Add type and suffix support to the product-text-field block * Add the placeholder to the external url input and remove required constraint to the buy button text * Set the url icon link type to external * Fix input border to be red when invalida now that base control is not present twice * Set the min height to 36px to match others non InputControls components height * Extends required constrain to also support a custom error message * Extends the product-text-field validation system * Add product-text-field documentation * Add changelog files * Fix php linter error * Fix compilation error * Fix linter errors --- .../js/product-editor/changelog/add-35148 | 4 + .../src/blocks/generic/text/README.md | 157 +++++++++++++--- .../src/blocks/generic/text/block.json | 31 ++-- .../src/blocks/generic/text/edit.tsx | 167 +++++++++++++----- .../src/blocks/generic/text/editor.scss | 10 ++ .../src/blocks/generic/text/types.ts | 14 +- .../src/components/block-editor/style.scss | 19 ++ .../js/product-editor/src/components/index.ts | 5 + .../src/components/text-control/index.ts | 1 + .../components/text-control/text-control.tsx | 78 +++----- .../src/components/text-control/types.ts | 19 ++ .../variations-table-row.tsx | 1 - plugins/woocommerce/changelog/add-35148 | 4 + .../Features/ProductBlockEditor/Init.php | 4 + .../SimpleProductTemplate.php | 77 +++++++- 15 files changed, 452 insertions(+), 139 deletions(-) create mode 100644 packages/js/product-editor/changelog/add-35148 create mode 100644 packages/js/product-editor/src/blocks/generic/text/editor.scss create mode 100644 packages/js/product-editor/src/components/text-control/types.ts create mode 100644 plugins/woocommerce/changelog/add-35148 diff --git a/packages/js/product-editor/changelog/add-35148 b/packages/js/product-editor/changelog/add-35148 new file mode 100644 index 00000000000..0d69ce52717 --- /dev/null +++ b/packages/js/product-editor/changelog/add-35148 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add support for custom validation message to the generic text block diff --git a/packages/js/product-editor/src/blocks/generic/text/README.md b/packages/js/product-editor/src/blocks/generic/text/README.md index 53440bd1ea8..982563ff8c6 100644 --- a/packages/js/product-editor/src/blocks/generic/text/README.md +++ b/packages/js/product-editor/src/blocks/generic/text/README.md @@ -8,66 +8,171 @@ A reusable text field for the product editor. ### label -- **Type:** `String` -- **Required:** `Yes` +- **Type:** `String` +- **Required:** `Yes` Label that appears on top of the field. ### property -- **Type:** `String` -- **Required:** `Yes` +- **Type:** `String` +- **Required:** `Yes` Property in which the value is stored. - ### help -- **Type:** `String` -- **Required:** `No` +- **Type:** `String` +- **Required:** `No` Help text that appears below the field. ### required -- **Type:** `Boolean` -- **Required:** `No` +- **Type:** `Boolean`|`String` +- **Required:** `No` Indicates and enforces that the field is required. +If the value is string it will be used as the custom error message. ### tooltip -- **Type:** `String` -- **Required:** `No` +- **Type:** `String` +- **Required:** `No` If provided, shows a tooltip next to the label with additional information. ### placeholder -- **Type:** `String` -- **Required:** `No` +- **Type:** `String` +- **Required:** `No` Placeholder text that appears in the field when it's empty. +### type + +- **Type:** `Object` + - `value` + - **Type:** `String` + - **Required:** `No` + - **Default:** `'text'` + - `message` + - **Type:** `String` + - **Required:** `No` +- **Required:** `No` + +Reffers to the type of the input. The `value` can be [any valid input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types). The message is used as a custom error `message` for the [typeMismatch](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/typeMismatch) validity. + +### pattern + +- **Type:** `Object` + - `value` + - **Type:** `String` + - **Required:** `Yes` + - `message` + - **Type:** `String` + - **Required:** `No` +- **Required:** `No` + +Reffers to the validation pattern of the input. The `value` can be [any valid regular expression](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern). The `message` is used as a custom error message for the [patternMismatch](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/patternMismatch) validity. + +### minLength + +- **Type:** `Object` + - `value` + - **Type:** `Number` + - **Required:** `Yes` + - `message` + - **Type:** `String` + - **Required:** `No` +- **Required:** `No` + +Reffers to the minimum string length constraint of the input. The `value` can be [any positive integer](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/minLength). The `message` is used as a custom error message for the [tooShort](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/tooShort) validity. + +### maxLength + +- **Type:** `Object` + - `value` + - **Type:** `Number` + - **Required:** `Yes` + - `message` + - **Type:** `String` + - **Required:** `No` +- **Required:** `No` + +Reffers to the maximum string length constraint of the input. The `value` can be [any positive integer](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/maxLength). The `message` is used as a custom error message for the [tooLong](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/tooLong) validity. + +### min + +- **Type:** `Object` + - `value` + - **Type:** `Number` + - **Required:** `Yes` + - `message` + - **Type:** `String` + - **Required:** `No` +- **Required:** `No` + +Reffers to the [minimum](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/min) value that is acceptable and valid for the input containing the attribute. The `value` must be less than or equal to the value of the `max` attribute. The `message` is used as a custom error message for the [rangeUnderflow](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/rangeUnderflow) validity. + +### max + +- **Type:** `Object` + - `value` + - **Type:** `Number` + - **Required:** `Yes` + - `message` + - **Type:** `String` + - **Required:** `No` +- **Required:** `No` + +Reffers to the [maximum](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/max) value that is acceptable and valid for the input containing the attribute. The `value` must be greater than or equal to the value of the `min` attribute. The `message` is used as a custom error message for the [rangeOverflow](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/rangeOverflow) validity. + ## Usage Here's a snippet that adds a field similar to the previous screenshot: ```php $section->add_block( - [ - 'id' => 'example-text-meta', - 'blockName' => 'woocommerce/product-text-field', - 'order' => 13, - 'attributes' => [ - 'label' => 'Text', - 'property' => 'meta_data.text', - 'placeholder' => 'Placeholder', - 'required' => true, - 'help' => 'Add additional information here', - 'tooltip' => 'My tooltip' - ], - ] + array( + 'id' => 'example-text-meta', + 'blockName' => 'woocommerce/product-text-field', + 'order' => 13, + 'attributes' => array( + 'label' => 'Text', + 'property' => 'meta_data.text', + 'placeholder' => 'Placeholder', + 'required' => true, + 'help' => 'Add additional information here', + 'tooltip' => 'My tooltip', + ), + ) ); ``` +Here's a snippet that adds fields validations: + +```php +$section->add_block( + array( + 'id' => 'product-external-url', + 'blockName' => 'woocommerce/product-text-field', + 'order' => 10, + 'attributes' => array( + 'property' => 'external_url', + 'label' => __( 'Link to the external product', 'woocommerce' ), + 'placeholder' => __( 'Enter the external URL to the product', 'woocommerce' ), + 'suffix' => true, + 'type' => array( + 'value' => 'url', + 'message' => __( 'Link to the external product is an invalid URL.', 'woocommerce' ), + ), + 'minLength' => array( + 'value' => 8, + 'message' => __( 'The link must be longer than %d.', 'woocommerce' ), + ), + 'required' => __( 'Link to the external product is required.', 'woocommerce' ), + ), + ) +); +``` diff --git a/packages/js/product-editor/src/blocks/generic/text/block.json b/packages/js/product-editor/src/blocks/generic/text/block.json index a9570dc78b6..4a2f037fe73 100644 --- a/packages/js/product-editor/src/blocks/generic/text/block.json +++ b/packages/js/product-editor/src/blocks/generic/text/block.json @@ -5,10 +5,7 @@ "title": "Product text field", "category": "woocommerce", "description": "A text field for use in the product editor.", - "keywords": [ - "products", - "text" - ], + "keywords": [ "products", "text" ], "textdomain": "default", "attributes": { "label": { @@ -27,21 +24,29 @@ "tooltip": { "type": "string" }, + "suffix": { + "type": "object" + }, + "type": { + "type": "object" + }, "required": { - "type": "boolean", - "default": false + "type": "object" }, - "validationRegex": { - "type": "string" - }, - "validationErrorMessage": { - "type": "string" + "pattern": { + "type": "object" }, "minLength": { - "type": "number" + "type": "object" }, "maxLength": { - "type": "number" + "type": "object" + }, + "min": { + "type": "object" + }, + "max": { + "type": "object" } }, "supports": { diff --git a/packages/js/product-editor/src/blocks/generic/text/edit.tsx b/packages/js/product-editor/src/blocks/generic/text/edit.tsx index 217a97296ba..e1f75ce8170 100644 --- a/packages/js/product-editor/src/blocks/generic/text/edit.tsx +++ b/packages/js/product-editor/src/blocks/generic/text/edit.tsx @@ -1,92 +1,167 @@ /** * External dependencies */ -import { createElement } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { Product } from '@woocommerce/data'; import { useWooBlockProps } from '@woocommerce/block-templates'; +import { Link } from '@woocommerce/components'; +import { Product } from '@woocommerce/data'; +import { createElement, useRef } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { Icon, external } from '@wordpress/icons'; /** * Internal dependencies */ -import { useValidation } from '../../../contexts/validation-context'; -import useProductEntityProp from '../../../hooks/use-product-entity-prop'; -import { TextBlockAttributes } from './types'; -import { ProductEditorBlockEditProps } from '../../../types'; import { TextControl } from '../../../components/text-control'; +import { useValidation } from '../../../contexts/validation-context'; import { useProductEdits } from '../../../hooks/use-product-edits'; +import useProductEntityProp from '../../../hooks/use-product-entity-prop'; +import { ProductEditorBlockEditProps } from '../../../types'; +import { TextBlockAttributes } from './types'; export function Edit( { attributes, context: { postType }, }: ProductEditorBlockEditProps< TextBlockAttributes > ) { const blockProps = useWooBlockProps( attributes ); + const { property, label, placeholder, required, - validationRegex, - validationErrorMessage, + pattern, minLength, maxLength, + min, + max, help, tooltip, disabled, + type, + suffix, } = attributes; + const [ value, setValue ] = useProductEntityProp< string >( property, { postType, fallbackValue: '', } ); + const { hasEdit } = useProductEdits(); + + const inputRef = useRef< HTMLInputElement >( null ); + const { error, validate } = useValidation< Product >( property, async function validator() { - if ( typeof value !== 'string' ) { - return __( - 'Unexpected property type assigned to field.', - 'woocommerce' + if ( ! inputRef.current ) return; + + const input = inputRef.current; + + let customErrorMessage = ''; + + if ( input.validity.typeMismatch ) { + customErrorMessage = + type?.message ?? + __( 'Invalid value for the field.', 'woocommerce' ); + } + if ( input.validity.valueMissing ) { + customErrorMessage = + typeof required === 'string' + ? required + : __( 'This field is required.', 'woocommerce' ); + } + if ( input.validity.patternMismatch ) { + customErrorMessage = + pattern?.message ?? + __( 'Invalid value for the field.', 'woocommerce' ); + } + if ( input.validity.tooShort ) { + // eslint-disable-next-line @wordpress/valid-sprintf + customErrorMessage = sprintf( + minLength?.message ?? + /* translators: %d: minimum length */ + __( + 'The minimum length of the field is %d', + 'woocommerce' + ), + minLength?.value ); } - if ( required && ! value ) { - return __( 'This field is required.', 'woocommerce' ); - } - if ( validationRegex ) { - const regExp = new RegExp( validationRegex ); - if ( ! regExp.test( value ) ) { - return ( - validationErrorMessage || - __( 'Invalid value for the field.', 'woocommerce' ) - ); - } - } - if ( typeof minLength === 'number' && value.length < minLength ) { - return sprintf( - /* translators: %d: minimum length */ - __( - 'The minimum length of the field is %d', - 'woocommerce' - ), - minLength + if ( input.validity.tooLong ) { + // eslint-disable-next-line @wordpress/valid-sprintf + customErrorMessage = sprintf( + maxLength?.message ?? + /* translators: %d: maximum length */ + __( + 'The maximum length of the field is %d', + 'woocommerce' + ), + maxLength?.value ); } - if ( typeof maxLength === 'number' && value.length > maxLength ) { - return sprintf( - /* translators: %d: maximum length */ - __( - 'The maximum length of the field is %d', - 'woocommerce' - ), - maxLength + if ( input.validity.rangeUnderflow ) { + // eslint-disable-next-line @wordpress/valid-sprintf + customErrorMessage = sprintf( + min?.message ?? + /* translators: %d: minimum length */ + __( + 'The minimum value of the field is %d', + 'woocommerce' + ), + min?.value ); } + if ( input.validity.rangeOverflow ) { + // eslint-disable-next-line @wordpress/valid-sprintf + customErrorMessage = sprintf( + max?.message ?? + /* translators: %d: maximum length */ + __( + 'The maximum value of the field is %d', + 'woocommerce' + ), + max?.value + ); + } + + input.setCustomValidity( customErrorMessage ); + + if ( ! input.validity.valid ) { + return input.validationMessage; + } }, - [ value ] + [ type, required, pattern, minLength, maxLength, min, max ] ); + function getSuffix() { + if ( ! suffix || ! value || ! inputRef.current ) return; + + const isValidUrl = + inputRef.current.type === 'url' && + ! inputRef.current.validity.typeMismatch; + + if ( suffix === true && isValidUrl ) { + return ( + + + + ); + } + + return typeof suffix === 'string' ? suffix : undefined; + } + return (
); diff --git a/packages/js/product-editor/src/blocks/generic/text/editor.scss b/packages/js/product-editor/src/blocks/generic/text/editor.scss new file mode 100644 index 00000000000..8dee0e91fe6 --- /dev/null +++ b/packages/js/product-editor/src/blocks/generic/text/editor.scss @@ -0,0 +1,10 @@ +.wp-block-woocommerce-product-text-field { + &__suffix-link { + display: flex; + align-items: center; + justify-content: center; + width: $grid-unit-30; + height: $grid-unit-30; + fill: $gray-700; + } +} diff --git a/packages/js/product-editor/src/blocks/generic/text/types.ts b/packages/js/product-editor/src/blocks/generic/text/types.ts index 9893417c547..3ecb84d1f9e 100644 --- a/packages/js/product-editor/src/blocks/generic/text/types.ts +++ b/packages/js/product-editor/src/blocks/generic/text/types.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import type { HTMLInputTypeAttribute } from 'react'; import type { BlockAttributes } from '@wordpress/blocks'; export interface TextBlockAttributes extends BlockAttributes { @@ -9,9 +10,12 @@ export interface TextBlockAttributes extends BlockAttributes { help?: string; tooltip?: string; placeholder?: string; - required?: boolean; - validationRegex?: string; - validationErrorMessage?: string; - minLength?: number; - maxLength?: number; + suffix?: boolean | string; + required?: boolean | string; + type?: { value?: HTMLInputTypeAttribute; message?: string }; + pattern?: { value: string; message?: string }; + minLength?: { value: number; message?: string }; + maxLength?: { value: number; message?: string }; + min?: { value: number; message?: string }; + max?: { value: number; message?: string }; } diff --git a/packages/js/product-editor/src/components/block-editor/style.scss b/packages/js/product-editor/src/components/block-editor/style.scss index 13add6c3d87..d53ad873a7f 100644 --- a/packages/js/product-editor/src/components/block-editor/style.scss +++ b/packages/js/product-editor/src/components/block-editor/style.scss @@ -42,6 +42,25 @@ max-width: unset; } + .components-base-control { + &.has-error { + .components-input-control__backdrop { + border-color: $studio-red-50; + } + + .components-base-control__help { + color: $studio-red-50; + } + } + + .components-input-control__container .components-input-control__input { + min-height: $grid-unit-40 + $grid-unit-05; + } + } + + // This is wrong for @wordpress/components/InputControl since it is + // wrapped within the BaseControl by default. So it does not need + // to be wrapped again. .has-error { .components-base-control { margin-bottom: 0; diff --git a/packages/js/product-editor/src/components/index.ts b/packages/js/product-editor/src/components/index.ts index cbb73ec4d2a..f054a6a3242 100644 --- a/packages/js/product-editor/src/components/index.ts +++ b/packages/js/product-editor/src/components/index.ts @@ -46,3 +46,8 @@ export { NumberControl as __experimentalNumberControl } from './number-control'; export * from './product-page-skeleton'; export * from './modal-editor-welcome-guide'; + +export { + TextControl as __experimentalTextControl, + TextControlProps, +} from './text-control'; diff --git a/packages/js/product-editor/src/components/text-control/index.ts b/packages/js/product-editor/src/components/text-control/index.ts index 4463ff317ca..1d39f2d61bf 100644 --- a/packages/js/product-editor/src/components/text-control/index.ts +++ b/packages/js/product-editor/src/components/text-control/index.ts @@ -1 +1,2 @@ export * from './text-control'; +export * from './types'; diff --git a/packages/js/product-editor/src/components/text-control/text-control.tsx b/packages/js/product-editor/src/components/text-control/text-control.tsx index 2a7fa83005d..1fc71d829a1 100644 --- a/packages/js/product-editor/src/components/text-control/text-control.tsx +++ b/packages/js/product-editor/src/components/text-control/text-control.tsx @@ -1,52 +1,41 @@ /** * External dependencies */ -import { createElement } from '@wordpress/element'; -import { useInstanceId } from '@wordpress/compose'; +import { Ref } from 'react'; +import { createElement, forwardRef } from '@wordpress/element'; import classNames from 'classnames'; import { - BaseControl, // @ts-expect-error `__experimentalInputControl` does exist. __experimentalInputControl as InputControl, } from '@wordpress/components'; + /** * Internal dependencies */ import { Label } from '../label/label'; +import { TextControlProps } from './types'; -export type TextProps = { - value?: string; - onChange: ( selected: string ) => void; - label: string; - suffix?: string; - help?: string; - error?: string; - placeholder?: string; - required?: boolean; - onBlur?: () => void; - tooltip?: string; - disabled?: boolean; -}; - -export const TextControl: React.FC< TextProps > = ( { - value, - onChange, - label, - help, - error, - onBlur, - placeholder, - required, - tooltip, - disabled, -}: TextProps ) => { - const textControlId = useInstanceId( - BaseControl, - 'text-control' - ) as string; +export const TextControl = forwardRef( function ForwardedTextControl( + { + label, + help, + error, + tooltip, + className, + required, + onChange, + onBlur, + ...props + }: TextControlProps, + ref: Ref< HTMLInputElement > +) { return ( - = ( { tooltip={ tooltip } /> } - className={ classNames( { - 'has-error': error, - } ) } + required={ required } help={ error || help } - > - - + onChange={ onChange } + onBlur={ onBlur } + /> ); -}; +} ); diff --git a/packages/js/product-editor/src/components/text-control/types.ts b/packages/js/product-editor/src/components/text-control/types.ts new file mode 100644 index 00000000000..f791bf723d9 --- /dev/null +++ b/packages/js/product-editor/src/components/text-control/types.ts @@ -0,0 +1,19 @@ +/** + * This must inherit the InputControlProp from @wordpress/components + * but it has not been exported yet + */ +export type TextControlProps = Omit< + React.DetailedHTMLProps< + React.InputHTMLAttributes< HTMLInputElement >, + HTMLInputElement + >, + 'onChange' +> & { + label: string; + help?: string; + error?: string; + tooltip?: string; + prefix?: React.ReactNode; + suffix?: React.ReactNode; + onChange?( value: string ): void; +}; diff --git a/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx b/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx index b3747fd7d9e..9c32feafffa 100644 --- a/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx +++ b/packages/js/product-editor/src/components/variations-table/variations-table-row/variations-table-row.tsx @@ -205,7 +205,6 @@ export function VariationsTableRow( { { ( variation.status === 'private' || ! variation.regular_price ) && ( supported_post_types, 'variable' ); } + if ( Features::is_enabled( 'product-external-affiliate' ) ) { + array_push( $this->supported_post_types, 'external' ); + } + $this->redirection_controller = new RedirectionController( $this->supported_post_types ); if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) { diff --git a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php index a43c5a12916..f948b2999aa 100644 --- a/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php +++ b/plugins/woocommerce/src/Internal/Features/ProductBlockEditor/ProductTemplates/SimpleProductTemplate.php @@ -226,11 +226,84 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ 'order' => 10, ) ); + + // External/Affiliate section. + if ( Features::is_enabled( 'product-external-affiliate' ) ) { + $buy_button_section = $general_group->add_section( + array( + 'id' => 'product-buy-button-section', + 'order' => 30, + 'attributes' => array( + 'title' => __( 'Buy button', 'woocommerce' ), + 'description' => __( 'Add a link and choose a label for the button linked to a product sold elsewhere.', 'woocommerce' ), + ), + 'hideConditions' => array( + array( + 'expression' => 'editedProduct.type !== "external"', + ), + ), + ) + ); + + $buy_button_section->add_block( + array( + 'id' => 'product-external-url', + 'blockName' => 'woocommerce/product-text-field', + 'order' => 10, + 'attributes' => array( + 'property' => 'external_url', + 'label' => __( 'Link to the external product', 'woocommerce' ), + 'placeholder' => __( 'Enter the external URL to the product', 'woocommerce' ), + 'suffix' => true, + 'type' => array( + 'value' => 'url', + 'message' => __( 'Link to the external product is an invalid URL.', 'woocommerce' ), + ), + 'required' => __( 'Link to the external product is required.', 'woocommerce' ), + ), + ) + ); + + $button_text_columns = $buy_button_section->add_block( + array( + 'id' => 'product-button-text-columns', + 'blockName' => 'core/columns', + 'order' => 20, + ) + ); + + $button_text_columns->add_block( + array( + 'id' => 'product-button-text-column1', + 'blockName' => 'core/column', + 'order' => 10, + ) + )->add_block( + array( + 'id' => 'product-button-text', + 'blockName' => 'woocommerce/product-text-field', + 'order' => 10, + 'attributes' => array( + 'property' => 'button_text', + 'label' => __( 'Buy button text', 'woocommerce' ), + ), + ) + ); + + $button_text_columns->add_block( + array( + 'id' => 'product-button-text-column2', + 'blockName' => 'core/column', + 'order' => 20, + ) + ); + } + // Images section. $images_section = $general_group->add_section( array( 'id' => 'product-images-section', - 'order' => 30, + 'order' => 40, 'attributes' => array( 'title' => __( 'Images', 'woocommerce' ), 'description' => sprintf( @@ -258,7 +331,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ $general_group->add_section( array( 'id' => 'product-downloads-section', - 'order' => 40, + 'order' => 50, 'attributes' => array( 'title' => __( 'Downloads', 'woocommerce' ), 'description' => __( "Add any files you'd like to make available for the customer to download after purchasing, such as instructions or warranty info.", 'woocommerce' ),