From 2957458bac0a4d488c86aa51c6c01b76e891619c Mon Sep 17 00:00:00 2001 From: Matt Sherman Date: Tue, 9 Jul 2024 14:05:39 -0400 Subject: [PATCH] Product Editor Dev Tools: Improved expression evaluation tooling (#48588) * Refactor to extract ExpressionField * Move styling of expression and result prefixes to CSS * Layout expression field with grid * Add buttons to expression field * Enter edit mode and cancel * Hook up editing in textarea * Allow existing expression to be edited * Make expression param optional * Use ExpressionField to add new expression * Hide cancel button if no callback and add updateLabel prop * Only show error when dirty * Placeholder for expression field * Format result in expression field * Expression field textarea styling * Justify expressions at bottom * Prevent jumping when editing expression * Revert changes on cancel * Remove unused CSS * Extract ExpressionTextArea * Extract ExpressionResult * Auto height textarea * Hover styling for expression * Result type tag * Tweak styling * Update padding in expression field * Changelog * Enable scrolling of result when editing expression * Use smaller close icon * Fix enabling of add/edit button * Focus text area when clicking edit button * Put cursor at the end of text area when focusing it * Enter edit mode when clicking on text area * Remove expression in list * Don't style expressions list as a flex box * Show full result while editing expression --- .../update-dev-tools-expression-evaluation | 4 + .../expression-field.tsx | 137 ++++++++++++++++++ .../expression-result.tsx | 34 +++++ .../expression-text-area.tsx | 51 +++++++ .../expressions-panel.tsx | 131 +++++++++-------- .../src/product-editor-dev-tools/index.scss | 106 ++++++++++++-- 6 files changed, 386 insertions(+), 77 deletions(-) create mode 100644 plugins/woocommerce-beta-tester/changelog/update-dev-tools-expression-evaluation create mode 100644 plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-field.tsx create mode 100644 plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-result.tsx create mode 100644 plugins/woocommerce-beta-tester/src/product-editor-dev-tools/expression-text-area.tsx 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 ( +