[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
This commit is contained in:
parent
89514921f9
commit
ed0d38c44b
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add support for custom validation message to the generic text block
|
|
@ -20,7 +20,6 @@ Label that appears on top of the field.
|
||||||
|
|
||||||
Property in which the value is stored.
|
Property in which the value is stored.
|
||||||
|
|
||||||
|
|
||||||
### help
|
### help
|
||||||
|
|
||||||
- **Type:** `String`
|
- **Type:** `String`
|
||||||
|
@ -30,10 +29,11 @@ Help text that appears below the field.
|
||||||
|
|
||||||
### required
|
### required
|
||||||
|
|
||||||
- **Type:** `Boolean`
|
- **Type:** `Boolean`|`String`
|
||||||
- **Required:** `No`
|
- **Required:** `No`
|
||||||
|
|
||||||
Indicates and enforces that the field is required.
|
Indicates and enforces that the field is required.
|
||||||
|
If the value is string it will be used as the custom error message.
|
||||||
|
|
||||||
### tooltip
|
### tooltip
|
||||||
|
|
||||||
|
@ -49,25 +49,130 @@ If provided, shows a tooltip next to the label with additional information.
|
||||||
|
|
||||||
Placeholder text that appears in the field when it's empty.
|
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
|
## Usage
|
||||||
|
|
||||||
Here's a snippet that adds a field similar to the previous screenshot:
|
Here's a snippet that adds a field similar to the previous screenshot:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$section->add_block(
|
$section->add_block(
|
||||||
[
|
array(
|
||||||
'id' => 'example-text-meta',
|
'id' => 'example-text-meta',
|
||||||
'blockName' => 'woocommerce/product-text-field',
|
'blockName' => 'woocommerce/product-text-field',
|
||||||
'order' => 13,
|
'order' => 13,
|
||||||
'attributes' => [
|
'attributes' => array(
|
||||||
'label' => 'Text',
|
'label' => 'Text',
|
||||||
'property' => 'meta_data.text',
|
'property' => 'meta_data.text',
|
||||||
'placeholder' => 'Placeholder',
|
'placeholder' => 'Placeholder',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'help' => 'Add additional information here',
|
'help' => 'Add additional information here',
|
||||||
'tooltip' => 'My tooltip'
|
'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' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
|
@ -5,10 +5,7 @@
|
||||||
"title": "Product text field",
|
"title": "Product text field",
|
||||||
"category": "woocommerce",
|
"category": "woocommerce",
|
||||||
"description": "A text field for use in the product editor.",
|
"description": "A text field for use in the product editor.",
|
||||||
"keywords": [
|
"keywords": [ "products", "text" ],
|
||||||
"products",
|
|
||||||
"text"
|
|
||||||
],
|
|
||||||
"textdomain": "default",
|
"textdomain": "default",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"label": {
|
"label": {
|
||||||
|
@ -27,21 +24,29 @@
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"suffix": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"required": {
|
"required": {
|
||||||
"type": "boolean",
|
"type": "object"
|
||||||
"default": false
|
|
||||||
},
|
},
|
||||||
"validationRegex": {
|
"pattern": {
|
||||||
"type": "string"
|
"type": "object"
|
||||||
},
|
|
||||||
"validationErrorMessage": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"minLength": {
|
"minLength": {
|
||||||
"type": "number"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"maxLength": {
|
"maxLength": {
|
||||||
"type": "number"
|
"type": "object"
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"supports": {
|
"supports": {
|
||||||
|
|
|
@ -1,92 +1,167 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement } from '@wordpress/element';
|
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import { Product } from '@woocommerce/data';
|
|
||||||
import { useWooBlockProps } from '@woocommerce/block-templates';
|
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
|
* 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 { TextControl } from '../../../components/text-control';
|
||||||
|
import { useValidation } from '../../../contexts/validation-context';
|
||||||
import { useProductEdits } from '../../../hooks/use-product-edits';
|
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( {
|
export function Edit( {
|
||||||
attributes,
|
attributes,
|
||||||
context: { postType },
|
context: { postType },
|
||||||
}: ProductEditorBlockEditProps< TextBlockAttributes > ) {
|
}: ProductEditorBlockEditProps< TextBlockAttributes > ) {
|
||||||
const blockProps = useWooBlockProps( attributes );
|
const blockProps = useWooBlockProps( attributes );
|
||||||
|
|
||||||
const {
|
const {
|
||||||
property,
|
property,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
required,
|
required,
|
||||||
validationRegex,
|
pattern,
|
||||||
validationErrorMessage,
|
|
||||||
minLength,
|
minLength,
|
||||||
maxLength,
|
maxLength,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
help,
|
help,
|
||||||
tooltip,
|
tooltip,
|
||||||
disabled,
|
disabled,
|
||||||
|
type,
|
||||||
|
suffix,
|
||||||
} = attributes;
|
} = attributes;
|
||||||
|
|
||||||
const [ value, setValue ] = useProductEntityProp< string >( property, {
|
const [ value, setValue ] = useProductEntityProp< string >( property, {
|
||||||
postType,
|
postType,
|
||||||
fallbackValue: '',
|
fallbackValue: '',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const { hasEdit } = useProductEdits();
|
const { hasEdit } = useProductEdits();
|
||||||
|
|
||||||
|
const inputRef = useRef< HTMLInputElement >( null );
|
||||||
|
|
||||||
const { error, validate } = useValidation< Product >(
|
const { error, validate } = useValidation< Product >(
|
||||||
property,
|
property,
|
||||||
async function validator() {
|
async function validator() {
|
||||||
if ( typeof value !== 'string' ) {
|
if ( ! inputRef.current ) return;
|
||||||
return __(
|
|
||||||
'Unexpected property type assigned to field.',
|
const input = inputRef.current;
|
||||||
'woocommerce'
|
|
||||||
);
|
let customErrorMessage = '';
|
||||||
|
|
||||||
|
if ( input.validity.typeMismatch ) {
|
||||||
|
customErrorMessage =
|
||||||
|
type?.message ??
|
||||||
|
__( 'Invalid value for the field.', 'woocommerce' );
|
||||||
}
|
}
|
||||||
if ( required && ! value ) {
|
if ( input.validity.valueMissing ) {
|
||||||
return __( 'This field is required.', 'woocommerce' );
|
customErrorMessage =
|
||||||
|
typeof required === 'string'
|
||||||
|
? required
|
||||||
|
: __( 'This field is required.', 'woocommerce' );
|
||||||
}
|
}
|
||||||
if ( validationRegex ) {
|
if ( input.validity.patternMismatch ) {
|
||||||
const regExp = new RegExp( validationRegex );
|
customErrorMessage =
|
||||||
if ( ! regExp.test( value ) ) {
|
pattern?.message ??
|
||||||
return (
|
__( 'Invalid value for the field.', 'woocommerce' );
|
||||||
validationErrorMessage ||
|
|
||||||
__( 'Invalid value for the field.', 'woocommerce' )
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
if ( input.validity.tooShort ) {
|
||||||
if ( typeof minLength === 'number' && value.length < minLength ) {
|
// eslint-disable-next-line @wordpress/valid-sprintf
|
||||||
return sprintf(
|
customErrorMessage = sprintf(
|
||||||
|
minLength?.message ??
|
||||||
/* translators: %d: minimum length */
|
/* translators: %d: minimum length */
|
||||||
__(
|
__(
|
||||||
'The minimum length of the field is %d',
|
'The minimum length of the field is %d',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
minLength
|
minLength?.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ( typeof maxLength === 'number' && value.length > maxLength ) {
|
if ( input.validity.tooLong ) {
|
||||||
return sprintf(
|
// eslint-disable-next-line @wordpress/valid-sprintf
|
||||||
|
customErrorMessage = sprintf(
|
||||||
|
maxLength?.message ??
|
||||||
/* translators: %d: maximum length */
|
/* translators: %d: maximum length */
|
||||||
__(
|
__(
|
||||||
'The maximum length of the field is %d',
|
'The maximum length of the field is %d',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
),
|
),
|
||||||
maxLength
|
maxLength?.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
if ( input.validity.rangeUnderflow ) {
|
||||||
[ value ]
|
// 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[ 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 (
|
||||||
|
<Link
|
||||||
|
type="external"
|
||||||
|
href={ value }
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="wp-block-woocommerce-product-text-field__suffix-link"
|
||||||
|
>
|
||||||
|
<Icon icon={ external } size={ 20 } />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof suffix === 'string' ? suffix : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
<TextControl
|
<TextControl
|
||||||
|
ref={ inputRef }
|
||||||
|
type={ type?.value ?? 'text' }
|
||||||
value={ value }
|
value={ value }
|
||||||
disabled={ disabled }
|
disabled={ disabled }
|
||||||
label={ label }
|
label={ label }
|
||||||
|
@ -99,8 +174,14 @@ export function Edit( {
|
||||||
error={ error }
|
error={ error }
|
||||||
help={ help }
|
help={ help }
|
||||||
placeholder={ placeholder }
|
placeholder={ placeholder }
|
||||||
required={ required }
|
|
||||||
tooltip={ tooltip }
|
tooltip={ tooltip }
|
||||||
|
suffix={ getSuffix() }
|
||||||
|
required={ Boolean( required ) }
|
||||||
|
pattern={ pattern?.value }
|
||||||
|
minLength={ minLength?.value }
|
||||||
|
maxLength={ maxLength?.value }
|
||||||
|
min={ min?.value }
|
||||||
|
max={ max?.value }
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import type { HTMLInputTypeAttribute } from 'react';
|
||||||
import type { BlockAttributes } from '@wordpress/blocks';
|
import type { BlockAttributes } from '@wordpress/blocks';
|
||||||
|
|
||||||
export interface TextBlockAttributes extends BlockAttributes {
|
export interface TextBlockAttributes extends BlockAttributes {
|
||||||
|
@ -9,9 +10,12 @@ export interface TextBlockAttributes extends BlockAttributes {
|
||||||
help?: string;
|
help?: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
required?: boolean;
|
suffix?: boolean | string;
|
||||||
validationRegex?: string;
|
required?: boolean | string;
|
||||||
validationErrorMessage?: string;
|
type?: { value?: HTMLInputTypeAttribute; message?: string };
|
||||||
minLength?: number;
|
pattern?: { value: string; message?: string };
|
||||||
maxLength?: number;
|
minLength?: { value: number; message?: string };
|
||||||
|
maxLength?: { value: number; message?: string };
|
||||||
|
min?: { value: number; message?: string };
|
||||||
|
max?: { value: number; message?: string };
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,25 @@
|
||||||
max-width: unset;
|
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 {
|
.has-error {
|
||||||
.components-base-control {
|
.components-base-control {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -46,3 +46,8 @@ export { NumberControl as __experimentalNumberControl } from './number-control';
|
||||||
export * from './product-page-skeleton';
|
export * from './product-page-skeleton';
|
||||||
|
|
||||||
export * from './modal-editor-welcome-guide';
|
export * from './modal-editor-welcome-guide';
|
||||||
|
|
||||||
|
export {
|
||||||
|
TextControl as __experimentalTextControl,
|
||||||
|
TextControlProps,
|
||||||
|
} from './text-control';
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from './text-control';
|
export * from './text-control';
|
||||||
|
export * from './types';
|
||||||
|
|
|
@ -1,52 +1,41 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement } from '@wordpress/element';
|
import { Ref } from 'react';
|
||||||
import { useInstanceId } from '@wordpress/compose';
|
import { createElement, forwardRef } from '@wordpress/element';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
BaseControl,
|
|
||||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||||
__experimentalInputControl as InputControl,
|
__experimentalInputControl as InputControl,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Label } from '../label/label';
|
import { Label } from '../label/label';
|
||||||
|
import { TextControlProps } from './types';
|
||||||
|
|
||||||
export type TextProps = {
|
export const TextControl = forwardRef( function ForwardedTextControl(
|
||||||
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,
|
label,
|
||||||
help,
|
help,
|
||||||
error,
|
error,
|
||||||
onBlur,
|
|
||||||
placeholder,
|
|
||||||
required,
|
|
||||||
tooltip,
|
tooltip,
|
||||||
disabled,
|
className,
|
||||||
}: TextProps ) => {
|
required,
|
||||||
const textControlId = useInstanceId(
|
onChange,
|
||||||
BaseControl,
|
onBlur,
|
||||||
'text-control'
|
...props
|
||||||
) as string;
|
}: TextControlProps,
|
||||||
|
ref: Ref< HTMLInputElement >
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<BaseControl
|
<InputControl
|
||||||
id={ textControlId }
|
{ ...props }
|
||||||
|
ref={ ref }
|
||||||
|
className={ classNames( className, {
|
||||||
|
'has-error': error,
|
||||||
|
} ) }
|
||||||
label={
|
label={
|
||||||
<Label
|
<Label
|
||||||
label={ label }
|
label={ label }
|
||||||
|
@ -54,19 +43,10 @@ export const TextControl: React.FC< TextProps > = ( {
|
||||||
tooltip={ tooltip }
|
tooltip={ tooltip }
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
className={ classNames( {
|
required={ required }
|
||||||
'has-error': error,
|
|
||||||
} ) }
|
|
||||||
help={ error || help }
|
help={ error || help }
|
||||||
>
|
|
||||||
<InputControl
|
|
||||||
id={ textControlId }
|
|
||||||
disabled={ disabled }
|
|
||||||
placeholder={ placeholder }
|
|
||||||
value={ value }
|
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
onBlur={ onBlur }
|
onBlur={ onBlur }
|
||||||
></InputControl>
|
/>
|
||||||
</BaseControl>
|
|
||||||
);
|
);
|
||||||
};
|
} );
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -205,7 +205,6 @@ export function VariationsTableRow( {
|
||||||
{ ( variation.status === 'private' ||
|
{ ( variation.status === 'private' ||
|
||||||
! variation.regular_price ) && (
|
! variation.regular_price ) && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
// @ts-expect-error className is missing in TS, should remove this when it is included.
|
|
||||||
className="woocommerce-attribute-list-item__actions-tooltip"
|
className="woocommerce-attribute-list-item__actions-tooltip"
|
||||||
position="top center"
|
position="top center"
|
||||||
text={ NOT_VISIBLE_TEXT }
|
text={ NOT_VISIBLE_TEXT }
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add support for external/affiliate products
|
|
@ -45,6 +45,10 @@ class Init {
|
||||||
array_push( $this->supported_post_types, 'variable' );
|
array_push( $this->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 );
|
$this->redirection_controller = new RedirectionController( $this->supported_post_types );
|
||||||
|
|
||||||
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
|
||||||
|
|
|
@ -226,11 +226,84 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||||
'order' => 10,
|
'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.
|
||||||
$images_section = $general_group->add_section(
|
$images_section = $general_group->add_section(
|
||||||
array(
|
array(
|
||||||
'id' => 'product-images-section',
|
'id' => 'product-images-section',
|
||||||
'order' => 30,
|
'order' => 40,
|
||||||
'attributes' => array(
|
'attributes' => array(
|
||||||
'title' => __( 'Images', 'woocommerce' ),
|
'title' => __( 'Images', 'woocommerce' ),
|
||||||
'description' => sprintf(
|
'description' => sprintf(
|
||||||
|
@ -258,7 +331,7 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
|
||||||
$general_group->add_section(
|
$general_group->add_section(
|
||||||
array(
|
array(
|
||||||
'id' => 'product-downloads-section',
|
'id' => 'product-downloads-section',
|
||||||
'order' => 40,
|
'order' => 50,
|
||||||
'attributes' => array(
|
'attributes' => array(
|
||||||
'title' => __( 'Downloads', 'woocommerce' ),
|
'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' ),
|
'description' => __( "Add any files you'd like to make available for the customer to download after purchasing, such as instructions or warranty info.", 'woocommerce' ),
|
||||||
|
|
Loading…
Reference in New Issue