diff --git a/plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation b/plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation new file mode 100644 index 00000000000..81868de608c --- /dev/null +++ b/plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Product Editor Dev Tools: Improve expression evaluation tooling support. diff --git a/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-field.tsx b/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-field.tsx new file mode 100644 index 00000000000..1bde26fa16d --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-field.tsx @@ -0,0 +1,137 @@ +/** + * External dependencies + */ +import { Button } from '@wordpress/components'; +import { useEffect, useRef, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { check, closeSmall, edit, trash } from '@wordpress/icons'; +import { evaluate } from '@woocommerce/expression-evaluation'; + +/** + * Internal dependencies + */ +import { ExpressionResult } from './expression-result'; +import { ExpressionTextArea } from './expression-text-area'; + +function evaluateExpression( + expression: string, + evaluationContext: object | undefined +) { + let result; + let error; + + try { + result = evaluate( expression, evaluationContext ); + } catch ( e ) { + error = e; + } + + return { + result, + error, + }; +} + +type ExpressionFieldProps = { + expression?: string; + evaluationContext?: object; + mode?: 'view' | 'edit'; + onEnterEdit?: () => void; + onUpdate?: ( expression: string ) => void; + onCancel?: () => void; + onRemove?: () => void; + updateLabel?: string; +}; + +export function ExpressionField( { + expression = '', + evaluationContext, + mode = 'view', + onEnterEdit, + onUpdate, + onCancel, + onRemove, + updateLabel = __( 'Update', 'woocommerce' ), +}: ExpressionFieldProps ) { + const textAreaRef = useRef< HTMLTextAreaElement >( null ); + + const [ editedExpression, setEditedExpression ] = useState( expression ); + + useEffect( () => setEditedExpression( expression ), [ expression ] ); + + const { result, error } = evaluateExpression( + editedExpression, + evaluationContext + ); + + function handleOnClickEdit() { + const textArea = textAreaRef.current; + + if ( textArea ) { + textArea.focus(); + textArea.setSelectionRange( + textArea.value.length, + textArea.value.length + ); + } + + onEnterEdit?.(); + } + + function handleOnClickCancel() { + setEditedExpression( expression ); + onCancel?.(); + } + + return ( +
+ + 0 } + /> +
+ { mode === 'view' ? ( + <> +
+
+ ); +} diff --git a/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-result.tsx b/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-result.tsx new file mode 100644 index 00000000000..dac48bfae80 --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-result.tsx @@ -0,0 +1,34 @@ +type ExpressionResultProps = { + result: any; + error: any; + showIfError: boolean; +}; + +export function ExpressionResult( { + result, + error, + showIfError = true, +}: ExpressionResultProps ) { + const errorString = error && showIfError ? String( error ) : ''; + + const resultString = error + ? errorString + : JSON.stringify( result, null, 4 ); + + const resultTypeLabel = error ? 'Error' : typeof result; + + return ( +
+ { showIfError && ( + <> +
+ { resultTypeLabel } +
+
+ { resultString } +
+ + ) } +
+ ); +} diff --git a/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-text-area.tsx b/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-text-area.tsx new file mode 100644 index 00000000000..8ad0f83bd0d --- /dev/null +++ b/plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-text-area.tsx @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { + forwardRef, + useImperativeHandle, + useLayoutEffect, + useRef, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +type ExpressionTextAreaProps = { + expression?: string; + readOnly?: boolean; + onChange?: ( expression: string ) => void; + onClick?: () => void; +}; + +export const ExpressionTextArea = forwardRef< + HTMLTextAreaElement, + ExpressionTextAreaProps +>( ( { expression, readOnly = false, onChange, onClick }, outerRef ) => { + const textAreaRef = useRef< HTMLTextAreaElement >( null ); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + useImperativeHandle( outerRef, () => textAreaRef.current!, [] ); + + useLayoutEffect( () => { + const textArea = textAreaRef.current; + + if ( ! textArea || textArea.scrollHeight < 1 ) return; + + // Have to first set the height to auto and then set it to the scrollHeight + // to allow the textarea to shrink when lines are removed + textArea.style.height = 'auto'; + textArea.style.height = `${ textArea.scrollHeight }px`; + }, [ expression ] ); + + return ( +